Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"binding.go",
"default_validator.go",
"form.go",
"form_mapping.go",
"json.go",
"query.go",
"tags.go",
"xml.go",
],
importpath = "go-common/library/net/http/blademaster/binding",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/gopkg.in/go-playground/validator.v9:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"binding_test.go",
"example_test.go",
"validate_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = ["//vendor/github.com/stretchr/testify/assert:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//library/net/http/blademaster/binding/example:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,85 @@
package binding
import (
"net/http"
"strings"
"gopkg.in/go-playground/validator.v9"
)
// MIME
const (
MIMEJSON = "application/json"
MIMEHTML = "text/html"
MIMEXML = "application/xml"
MIMEXML2 = "text/xml"
MIMEPlain = "text/plain"
MIMEPOSTForm = "application/x-www-form-urlencoded"
MIMEMultipartPOSTForm = "multipart/form-data"
)
// Binding http binding request interface.
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
}
// StructValidator http validator interface.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned.
ValidateStruct(interface{}) error
// RegisterValidation adds a validation Func to a Validate's map of validators denoted by the key
// NOTE: if the key already exists, the previous validation function will be replaced.
// NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation
RegisterValidation(string, validator.Func) error
}
// Validator default validator.
var Validator StructValidator = &defaultValidator{}
// Binding
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
)
// Default get by binding type by method and contexttype.
func Default(method, contentType string) Binding {
if method == "GET" {
return Form
}
contentType = stripContentTypeParam(contentType)
switch contentType {
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
return XML
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
return Form
}
}
func validate(obj interface{}) error {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(obj)
}
func stripContentTypeParam(contentType string) string {
i := strings.Index(contentType, ";")
if i != -1 {
contentType = contentType[:i]
}
return contentType
}

View File

@@ -0,0 +1,342 @@
package binding
import (
"bytes"
"mime/multipart"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
type FooStruct struct {
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" validate:"required"`
}
type FooBarStruct struct {
FooStruct
Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" validate:"required"`
Slice []string `form:"slice" validate:"max=10"`
}
type ComplexDefaultStruct struct {
Int int `form:"int" default:"999"`
String string `form:"string" default:"default-string"`
Bool bool `form:"bool" default:"false"`
Int64Slice []int64 `form:"int64_slice,split" default:"1,2,3,4"`
Int8Slice []int8 `form:"int8_slice,split" default:"1,2,3,4"`
}
type Int8SliceStruct struct {
State []int8 `form:"state,split"`
}
type Int64SliceStruct struct {
State []int64 `form:"state,split"`
}
type StringSliceStruct struct {
State []string `form:"state,split"`
}
func TestBindingDefault(t *testing.T) {
assert.Equal(t, Default("GET", ""), Form)
assert.Equal(t, Default("GET", MIMEJSON), Form)
assert.Equal(t, Default("GET", MIMEJSON+"; charset=utf-8"), Form)
assert.Equal(t, Default("POST", MIMEJSON), JSON)
assert.Equal(t, Default("PUT", MIMEJSON), JSON)
assert.Equal(t, Default("POST", MIMEJSON+"; charset=utf-8"), JSON)
assert.Equal(t, Default("PUT", MIMEJSON+"; charset=utf-8"), JSON)
assert.Equal(t, Default("POST", MIMEXML), XML)
assert.Equal(t, Default("PUT", MIMEXML2), XML)
assert.Equal(t, Default("POST", MIMEPOSTForm), Form)
assert.Equal(t, Default("PUT", MIMEPOSTForm), Form)
assert.Equal(t, Default("POST", MIMEPOSTForm+"; charset=utf-8"), Form)
assert.Equal(t, Default("PUT", MIMEPOSTForm+"; charset=utf-8"), Form)
assert.Equal(t, Default("POST", MIMEMultipartPOSTForm), Form)
assert.Equal(t, Default("PUT", MIMEMultipartPOSTForm), Form)
}
func TestStripContentType(t *testing.T) {
c1 := "application/vnd.mozilla.xul+xml"
c2 := "application/vnd.mozilla.xul+xml; charset=utf-8"
assert.Equal(t, stripContentTypeParam(c1), c1)
assert.Equal(t, stripContentTypeParam(c2), "application/vnd.mozilla.xul+xml")
}
func TestBindInt8Form(t *testing.T) {
params := "state=1,2,3"
req, _ := http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q := new(Int8SliceStruct)
Form.Bind(req, q)
assert.EqualValues(t, []int8{1, 2, 3}, q.State)
params = "state=1,2,3,256"
req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q = new(Int8SliceStruct)
assert.Error(t, Form.Bind(req, q))
params = "state="
req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q = new(Int8SliceStruct)
assert.NoError(t, Form.Bind(req, q))
assert.Len(t, q.State, 0)
params = "state=1,,2"
req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q = new(Int8SliceStruct)
assert.NoError(t, Form.Bind(req, q))
assert.EqualValues(t, []int8{1, 2}, q.State)
}
func TestBindInt64Form(t *testing.T) {
params := "state=1,2,3"
req, _ := http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q := new(Int64SliceStruct)
Form.Bind(req, q)
assert.EqualValues(t, []int64{1, 2, 3}, q.State)
params = "state="
req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q = new(Int64SliceStruct)
assert.NoError(t, Form.Bind(req, q))
assert.Len(t, q.State, 0)
}
func TestBindStringForm(t *testing.T) {
params := "state=1,2,3"
req, _ := http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q := new(StringSliceStruct)
Form.Bind(req, q)
assert.EqualValues(t, []string{"1", "2", "3"}, q.State)
params = "state="
req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q = new(StringSliceStruct)
assert.NoError(t, Form.Bind(req, q))
assert.Len(t, q.State, 0)
params = "state=p,,p"
req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q = new(StringSliceStruct)
Form.Bind(req, q)
assert.EqualValues(t, []string{"p", "p"}, q.State)
}
func TestBindingJSON(t *testing.T) {
testBodyBinding(t,
JSON, "json",
"/", "/",
`{"foo": "bar"}`, `{"bar": "foo"}`)
}
func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST",
"/", "/",
"foo=bar&bar=foo&slice=a&slice=b", "bar2=foo")
}
func TestBindingForm2(t *testing.T) {
testFormBinding(t, "GET",
"/?foo=bar&bar=foo", "/?bar2=foo",
"", "")
}
func TestBindingQuery(t *testing.T) {
testQueryBinding(t, "POST",
"/?foo=bar&bar=foo", "/",
"foo=unused", "bar2=foo")
}
func TestBindingQuery2(t *testing.T) {
testQueryBinding(t, "GET",
"/?foo=bar&bar=foo", "/?bar2=foo",
"foo=unused", "")
}
func TestBindingXML(t *testing.T) {
testBodyBinding(t,
XML, "xml",
"/", "/",
"<map><foo>bar</foo></map>", "<map><bar>foo</bar></map>")
}
func createFormPostRequest() *http.Request {
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
func createFormMultipartRequest() *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
mw.SetBoundary(boundary)
mw.WriteField("foo", "bar")
mw.WriteField("bar", "foo")
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
func TestBindingFormPost(t *testing.T) {
req := createFormPostRequest()
var obj FooBarStruct
FormPost.Bind(req, &obj)
assert.Equal(t, obj.Foo, "bar")
assert.Equal(t, obj.Bar, "foo")
}
func TestBindingFormMultipart(t *testing.T) {
req := createFormMultipartRequest()
var obj FooBarStruct
FormMultipart.Bind(req, &obj)
assert.Equal(t, obj.Foo, "bar")
assert.Equal(t, obj.Bar, "foo")
}
func TestValidationFails(t *testing.T) {
var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`)
err := JSON.Bind(req, &obj)
assert.Error(t, err)
}
func TestValidationDisabled(t *testing.T) {
backup := Validator
Validator = nil
defer func() { Validator = backup }()
var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`)
err := JSON.Bind(req, &obj)
assert.NoError(t, err)
}
func TestExistsSucceeds(t *testing.T) {
type HogeStruct struct {
Hoge *int `json:"hoge" binding:"exists"`
}
var obj HogeStruct
req := requestWithBody("POST", "/", `{"hoge": 0}`)
err := JSON.Bind(req, &obj)
assert.NoError(t, err)
}
func TestFormDefaultValue(t *testing.T) {
params := "int=333&string=hello&bool=true&int64_slice=5,6,7,8&int8_slice=5,6,7,8"
req, _ := http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q := new(ComplexDefaultStruct)
assert.NoError(t, Form.Bind(req, q))
assert.Equal(t, 333, q.Int)
assert.Equal(t, "hello", q.String)
assert.Equal(t, true, q.Bool)
assert.EqualValues(t, []int64{5, 6, 7, 8}, q.Int64Slice)
assert.EqualValues(t, []int8{5, 6, 7, 8}, q.Int8Slice)
params = "string=hello&bool=false"
req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q = new(ComplexDefaultStruct)
assert.NoError(t, Form.Bind(req, q))
assert.Equal(t, 999, q.Int)
assert.Equal(t, "hello", q.String)
assert.Equal(t, false, q.Bool)
assert.EqualValues(t, []int64{1, 2, 3, 4}, q.Int64Slice)
assert.EqualValues(t, []int8{1, 2, 3, 4}, q.Int8Slice)
params = "strings=hello"
req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q = new(ComplexDefaultStruct)
assert.NoError(t, Form.Bind(req, q))
assert.Equal(t, 999, q.Int)
assert.Equal(t, "default-string", q.String)
assert.Equal(t, false, q.Bool)
assert.EqualValues(t, []int64{1, 2, 3, 4}, q.Int64Slice)
assert.EqualValues(t, []int8{1, 2, 3, 4}, q.Int8Slice)
params = "int=&string=&bool=true&int64_slice=&int8_slice="
req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
q = new(ComplexDefaultStruct)
assert.NoError(t, Form.Bind(req, q))
assert.Equal(t, 999, q.Int)
assert.Equal(t, "default-string", q.String)
assert.Equal(t, true, q.Bool)
assert.EqualValues(t, []int64{1, 2, 3, 4}, q.Int64Slice)
assert.EqualValues(t, []int8{1, 2, 3, 4}, q.Int8Slice)
}
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, b.Name(), "form")
obj := FooBarStruct{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, obj.Foo, "bar")
assert.Equal(t, obj.Bar, "foo")
obj = FooBarStruct{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
}
func testQueryBinding(t *testing.T, method, path, badPath, body, badBody string) {
b := Query
assert.Equal(t, b.Name(), "query")
obj := FooBarStruct{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, obj.Foo, "bar")
assert.Equal(t, obj.Bar, "foo")
}
func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, b.Name(), name)
obj := FooStruct{}
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, obj.Foo, "bar")
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
}
func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return
}
func BenchmarkBindingForm(b *testing.B) {
req := requestWithBody("POST", "/", "foo=bar&bar=foo&slice=a&slice=b&slice=c&slice=w")
req.Header.Add("Content-Type", MIMEPOSTForm)
f := Form
for i := 0; i < b.N; i++ {
obj := FooBarStruct{}
f.Bind(req, &obj)
}
}

View File

@@ -0,0 +1,45 @@
package binding
import (
"reflect"
"sync"
"gopkg.in/go-playground/validator.v9"
)
type defaultValidator struct {
once sync.Once
validate *validator.Validate
}
var _ StructValidator = &defaultValidator{}
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
if kindOfData(obj) == reflect.Struct {
v.lazyinit()
if err := v.validate.Struct(obj); err != nil {
return err
}
}
return nil
}
func (v *defaultValidator) RegisterValidation(key string, fn validator.Func) error {
v.lazyinit()
return v.validate.RegisterValidation(key, fn)
}
func (v *defaultValidator) lazyinit() {
v.once.Do(func() {
v.validate = validator.New()
})
}
func kindOfData(data interface{}) reflect.Kind {
value := reflect.ValueOf(data)
valueType := value.Kind()
if valueType == reflect.Ptr {
valueType = value.Elem().Kind()
}
return valueType
}

View File

@@ -0,0 +1,45 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
load(
"@io_bazel_rules_go//proto:def.bzl",
"go_proto_library",
)
proto_library(
name = "example_proto",
srcs = ["test.proto"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_proto_library(
name = "example_go_proto",
compilers = ["@io_bazel_rules_go//proto:go_proto"],
importpath = "go-common/library/net/http/blademaster/binding/example",
proto = ":example_proto",
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_library(
name = "go_default_library",
embed = [":example_go_proto"],
importpath = "go-common/net/http/blademaster/binding/example",
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,113 @@
// Code generated by protoc-gen-go.
// source: test.proto
// DO NOT EDIT!
/*
Package example is a generated protocol buffer package.
It is generated from these files:
test.proto
It has these top-level messages:
Test
*/
package example
import proto "github.com/golang/protobuf/proto"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = math.Inf
type FOO int32
const (
FOO_X FOO = 17
)
var FOO_name = map[int32]string{
17: "X",
}
var FOO_value = map[string]int32{
"X": 17,
}
func (x FOO) Enum() *FOO {
p := new(FOO)
*p = x
return p
}
func (x FOO) String() string {
return proto.EnumName(FOO_name, int32(x))
}
func (x *FOO) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(FOO_value, data, "FOO")
if err != nil {
return err
}
*x = FOO(value)
return nil
}
type Test struct {
Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"`
Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"`
Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"`
Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Test) Reset() { *m = Test{} }
func (m *Test) String() string { return proto.CompactTextString(m) }
func (*Test) ProtoMessage() {}
const Default_Test_Type int32 = 77
func (m *Test) GetLabel() string {
if m != nil && m.Label != nil {
return *m.Label
}
return ""
}
func (m *Test) GetType() int32 {
if m != nil && m.Type != nil {
return *m.Type
}
return Default_Test_Type
}
func (m *Test) GetReps() []int64 {
if m != nil {
return m.Reps
}
return nil
}
func (m *Test) GetOptionalgroup() *Test_OptionalGroup {
if m != nil {
return m.Optionalgroup
}
return nil
}
type Test_OptionalGroup struct {
RequiredField *string `protobuf:"bytes,5,req" json:"RequiredField,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *Test_OptionalGroup) Reset() { *m = Test_OptionalGroup{} }
func (m *Test_OptionalGroup) String() string { return proto.CompactTextString(m) }
func (*Test_OptionalGroup) ProtoMessage() {}
func (m *Test_OptionalGroup) GetRequiredField() string {
if m != nil && m.RequiredField != nil {
return *m.RequiredField
}
return ""
}
func init() {
proto.RegisterEnum("example.FOO", FOO_name, FOO_value)
}

View File

@@ -0,0 +1,12 @@
package example;
enum FOO {X=17;};
message Test {
required string label = 1;
optional int32 type = 2[default=77];
repeated int64 reps = 3;
optional group OptionalGroup = 4{
required string RequiredField = 5;
}
}

View File

@@ -0,0 +1,36 @@
package binding
import (
"fmt"
"log"
"net/http"
)
type Arg struct {
Max int64 `form:"max" validate:"max=10"`
Min int64 `form:"min" validate:"min=2"`
Range int64 `form:"range" validate:"min=1,max=10"`
// use split option to split arg 1,2,3 into slice [1 2 3]
// otherwise slice type with parse url.Values (eg:a=b&a=c) default.
Slice []int64 `form:"slice,split" validate:"min=1"`
}
func ExampleBinding() {
req := initHTTP("max=9&min=3&range=3&slice=1,2,3")
arg := new(Arg)
if err := Form.Bind(req, arg); err != nil {
log.Fatal(err)
}
fmt.Printf("arg.Max %d\narg.Min %d\narg.Range %d\narg.Slice %v", arg.Max, arg.Min, arg.Range, arg.Slice)
// Output:
// arg.Max 9
// arg.Min 3
// arg.Range 3
// arg.Slice [1 2 3]
}
func initHTTP(params string) (req *http.Request) {
req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil)
req.ParseForm()
return
}

View File

@@ -0,0 +1,55 @@
package binding
import (
"net/http"
"github.com/pkg/errors"
)
const defaultMemory = 32 * 1024 * 1024
type formBinding struct{}
type formPostBinding struct{}
type formMultipartBinding struct{}
func (f formBinding) Name() string {
return "form"
}
func (f formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil {
return errors.WithStack(err)
}
if err := mapForm(obj, req.Form); err != nil {
return err
}
return validate(obj)
}
func (f formPostBinding) Name() string {
return "form-urlencoded"
}
func (f formPostBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil {
return errors.WithStack(err)
}
if err := mapForm(obj, req.PostForm); err != nil {
return err
}
return validate(obj)
}
func (f formMultipartBinding) Name() string {
return "multipart/form-data"
}
func (f formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseMultipartForm(defaultMemory); err != nil {
return errors.WithStack(err)
}
if err := mapForm(obj, req.MultipartForm.Value); err != nil {
return err
}
return validate(obj)
}

View File

@@ -0,0 +1,276 @@
package binding
import (
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/pkg/errors"
)
// scache struct reflect type cache.
var scache = &cache{
data: make(map[reflect.Type]*sinfo),
}
type cache struct {
data map[reflect.Type]*sinfo
mutex sync.RWMutex
}
func (c *cache) get(obj reflect.Type) (s *sinfo) {
var ok bool
c.mutex.RLock()
if s, ok = c.data[obj]; !ok {
c.mutex.RUnlock()
s = c.set(obj)
return
}
c.mutex.RUnlock()
return
}
func (c *cache) set(obj reflect.Type) (s *sinfo) {
s = new(sinfo)
tp := obj.Elem()
for i := 0; i < tp.NumField(); i++ {
fd := new(field)
fd.tp = tp.Field(i)
tag := fd.tp.Tag.Get("form")
fd.name, fd.option = parseTag(tag)
if defV := fd.tp.Tag.Get("default"); defV != "" {
dv := reflect.New(fd.tp.Type).Elem()
setWithProperType(fd.tp.Type.Kind(), []string{defV}, dv, fd.option)
fd.hasDefault = true
fd.defaultValue = dv
}
s.field = append(s.field, fd)
}
c.mutex.Lock()
c.data[obj] = s
c.mutex.Unlock()
return
}
type sinfo struct {
field []*field
}
type field struct {
tp reflect.StructField
name string
option tagOptions
hasDefault bool // if field had default value
defaultValue reflect.Value // field default value
}
func mapForm(ptr interface{}, form map[string][]string) error {
sinfo := scache.get(reflect.TypeOf(ptr))
val := reflect.ValueOf(ptr).Elem()
for i, fd := range sinfo.field {
typeField := fd.tp
structField := val.Field(i)
if !structField.CanSet() {
continue
}
structFieldKind := structField.Kind()
inputFieldName := fd.name
if inputFieldName == "" {
inputFieldName = typeField.Name
// if "form" tag is nil, we inspect if the field is a struct.
// this would not make sense for JSON parsing but it does for a form
// since data is flatten
if structFieldKind == reflect.Struct {
err := mapForm(structField.Addr().Interface(), form)
if err != nil {
return err
}
continue
}
}
inputValue, exists := form[inputFieldName]
if !exists {
// Set the field as default value when the input value is not exist
if fd.hasDefault {
structField.Set(fd.defaultValue)
}
continue
}
// Set the field as default value when the input value is empty
if fd.hasDefault && inputValue[0] == "" {
structField.Set(fd.defaultValue)
continue
}
if _, isTime := structField.Interface().(time.Time); isTime {
if err := setTimeField(inputValue[0], typeField, structField); err != nil {
return err
}
continue
}
if err := setWithProperType(typeField.Type.Kind(), inputValue, structField, fd.option); err != nil {
return err
}
}
return nil
}
func setWithProperType(valueKind reflect.Kind, val []string, structField reflect.Value, option tagOptions) error {
switch valueKind {
case reflect.Int:
return setIntField(val[0], 0, structField)
case reflect.Int8:
return setIntField(val[0], 8, structField)
case reflect.Int16:
return setIntField(val[0], 16, structField)
case reflect.Int32:
return setIntField(val[0], 32, structField)
case reflect.Int64:
return setIntField(val[0], 64, structField)
case reflect.Uint:
return setUintField(val[0], 0, structField)
case reflect.Uint8:
return setUintField(val[0], 8, structField)
case reflect.Uint16:
return setUintField(val[0], 16, structField)
case reflect.Uint32:
return setUintField(val[0], 32, structField)
case reflect.Uint64:
return setUintField(val[0], 64, structField)
case reflect.Bool:
return setBoolField(val[0], structField)
case reflect.Float32:
return setFloatField(val[0], 32, structField)
case reflect.Float64:
return setFloatField(val[0], 64, structField)
case reflect.String:
structField.SetString(val[0])
case reflect.Slice:
if option.Contains("split") {
val = strings.Split(val[0], ",")
}
filtered := filterEmpty(val)
switch structField.Type().Elem().Kind() {
case reflect.Int64:
valSli := make([]int64, 0, len(filtered))
for i := 0; i < len(filtered); i++ {
d, err := strconv.ParseInt(filtered[i], 10, 64)
if err != nil {
return err
}
valSli = append(valSli, d)
}
structField.Set(reflect.ValueOf(valSli))
case reflect.String:
valSli := make([]string, 0, len(filtered))
for i := 0; i < len(filtered); i++ {
valSli = append(valSli, filtered[i])
}
structField.Set(reflect.ValueOf(valSli))
default:
sliceOf := structField.Type().Elem().Kind()
numElems := len(filtered)
slice := reflect.MakeSlice(structField.Type(), len(filtered), len(filtered))
for i := 0; i < numElems; i++ {
if err := setWithProperType(sliceOf, filtered[i:], slice.Index(i), ""); err != nil {
return err
}
}
structField.Set(slice)
}
default:
return errors.New("Unknown type")
}
return nil
}
func setIntField(val string, bitSize int, field reflect.Value) error {
if val == "" {
val = "0"
}
intVal, err := strconv.ParseInt(val, 10, bitSize)
if err == nil {
field.SetInt(intVal)
}
return errors.WithStack(err)
}
func setUintField(val string, bitSize int, field reflect.Value) error {
if val == "" {
val = "0"
}
uintVal, err := strconv.ParseUint(val, 10, bitSize)
if err == nil {
field.SetUint(uintVal)
}
return errors.WithStack(err)
}
func setBoolField(val string, field reflect.Value) error {
if val == "" {
val = "false"
}
boolVal, err := strconv.ParseBool(val)
if err == nil {
field.SetBool(boolVal)
}
return nil
}
func setFloatField(val string, bitSize int, field reflect.Value) error {
if val == "" {
val = "0.0"
}
floatVal, err := strconv.ParseFloat(val, bitSize)
if err == nil {
field.SetFloat(floatVal)
}
return errors.WithStack(err)
}
func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
timeFormat := structField.Tag.Get("time_format")
if timeFormat == "" {
return errors.New("Blank time format")
}
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil
}
l := time.Local
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
l = time.UTC
}
if locTag := structField.Tag.Get("time_location"); locTag != "" {
loc, err := time.LoadLocation(locTag)
if err != nil {
return errors.WithStack(err)
}
l = loc
}
t, err := time.ParseInLocation(timeFormat, val, l)
if err != nil {
return errors.WithStack(err)
}
value.Set(reflect.ValueOf(t))
return nil
}
func filterEmpty(val []string) []string {
filtered := make([]string, 0, len(val))
for _, v := range val {
if v != "" {
filtered = append(filtered, v)
}
}
return filtered
}

View File

@@ -0,0 +1,22 @@
package binding
import (
"encoding/json"
"net/http"
"github.com/pkg/errors"
)
type jsonBinding struct{}
func (jsonBinding) Name() string {
return "json"
}
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(obj); err != nil {
return errors.WithStack(err)
}
return validate(obj)
}

View File

@@ -0,0 +1,19 @@
package binding
import (
"net/http"
)
type queryBinding struct{}
func (queryBinding) Name() string {
return "query"
}
func (queryBinding) Bind(req *http.Request, obj interface{}) error {
values := req.URL.Query()
if err := mapForm(obj, values); err != nil {
return err
}
return validate(obj)
}

View File

@@ -0,0 +1,44 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package binding
import (
"strings"
)
// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string
// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
if idx := strings.Index(tag, ","); idx != -1 {
return tag[:idx], tagOptions(tag[idx+1:])
}
return tag, tagOptions("")
}
// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var next string
i := strings.Index(s, ",")
if i >= 0 {
s, next = s[:i], s[i+1:]
}
if s == optionName {
return true
}
s = next
}
return false
}

View File

@@ -0,0 +1,209 @@
package binding
import (
"bytes"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type testInterface interface {
String() string
}
type substructNoValidation struct {
IString string
IInt int
}
type mapNoValidationSub map[string]substructNoValidation
type structNoValidationValues struct {
substructNoValidation
Boolean bool
Uinteger uint
Integer int
Integer8 int8
Integer16 int16
Integer32 int32
Integer64 int64
Uinteger8 uint8
Uinteger16 uint16
Uinteger32 uint32
Uinteger64 uint64
Float32 float32
Float64 float64
String string
Date time.Time
Struct substructNoValidation
InlinedStruct struct {
String []string
Integer int
}
IntSlice []int
IntPointerSlice []*int
StructPointerSlice []*substructNoValidation
StructSlice []substructNoValidation
InterfaceSlice []testInterface
UniversalInterface interface{}
CustomInterface testInterface
FloatMap map[string]float32
StructMap mapNoValidationSub
}
func createNoValidationValues() structNoValidationValues {
integer := 1
s := structNoValidationValues{
Boolean: true,
Uinteger: 1 << 29,
Integer: -10000,
Integer8: 120,
Integer16: -20000,
Integer32: 1 << 29,
Integer64: 1 << 61,
Uinteger8: 250,
Uinteger16: 50000,
Uinteger32: 1 << 31,
Uinteger64: 1 << 62,
Float32: 123.456,
Float64: 123.456789,
String: "text",
Date: time.Time{},
CustomInterface: &bytes.Buffer{},
Struct: substructNoValidation{},
IntSlice: []int{-3, -2, 1, 0, 1, 2, 3},
IntPointerSlice: []*int{&integer},
StructSlice: []substructNoValidation{},
UniversalInterface: 1.2,
FloatMap: map[string]float32{
"foo": 1.23,
"bar": 232.323,
},
StructMap: mapNoValidationSub{
"foo": substructNoValidation{},
"bar": substructNoValidation{},
},
// StructPointerSlice []noValidationSub
// InterfaceSlice []testInterface
}
s.InlinedStruct.Integer = 1000
s.InlinedStruct.String = []string{"first", "second"}
s.IString = "substring"
s.IInt = 987654
return s
}
func TestValidateNoValidationValues(t *testing.T) {
origin := createNoValidationValues()
test := createNoValidationValues()
empty := structNoValidationValues{}
assert.Nil(t, validate(test))
assert.Nil(t, validate(&test))
assert.Nil(t, validate(empty))
assert.Nil(t, validate(&empty))
assert.Equal(t, origin, test)
}
type structNoValidationPointer struct {
// substructNoValidation
Boolean bool
Uinteger *uint
Integer *int
Integer8 *int8
Integer16 *int16
Integer32 *int32
Integer64 *int64
Uinteger8 *uint8
Uinteger16 *uint16
Uinteger32 *uint32
Uinteger64 *uint64
Float32 *float32
Float64 *float64
String *string
Date *time.Time
Struct *substructNoValidation
IntSlice *[]int
IntPointerSlice *[]*int
StructPointerSlice *[]*substructNoValidation
StructSlice *[]substructNoValidation
InterfaceSlice *[]testInterface
FloatMap *map[string]float32
StructMap *mapNoValidationSub
}
func TestValidateNoValidationPointers(t *testing.T) {
//origin := createNoValidation_values()
//test := createNoValidation_values()
empty := structNoValidationPointer{}
//assert.Nil(t, validate(test))
//assert.Nil(t, validate(&test))
assert.Nil(t, validate(empty))
assert.Nil(t, validate(&empty))
//assert.Equal(t, origin, test)
}
type Object map[string]interface{}
func TestValidatePrimitives(t *testing.T) {
obj := Object{"foo": "bar", "bar": 1}
assert.NoError(t, validate(obj))
assert.NoError(t, validate(&obj))
assert.Equal(t, obj, Object{"foo": "bar", "bar": 1})
obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}}
assert.NoError(t, validate(obj2))
assert.NoError(t, validate(&obj2))
nu := 10
assert.NoError(t, validate(nu))
assert.NoError(t, validate(&nu))
assert.Equal(t, nu, 10)
str := "value"
assert.NoError(t, validate(str))
assert.NoError(t, validate(&str))
assert.Equal(t, str, "value")
}
// structCustomValidation is a helper struct we use to check that
// custom validation can be registered on it.
// The `notone` binding directive is for custom validation and registered later.
// type structCustomValidation struct {
// Integer int `binding:"notone"`
// }
// notOne is a custom validator meant to be used with `validator.v8` library.
// The method signature for `v9` is significantly different and this function
// would need to be changed for tests to pass after upgrade.
// See https://github.com/gin-gonic/gin/pull/1015.
// func notOne(
// v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
// field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
// ) bool {
// if val, ok := field.Interface().(int); ok {
// return val != 1
// }
// return false
// }

View File

@@ -0,0 +1,22 @@
package binding
import (
"encoding/xml"
"net/http"
"github.com/pkg/errors"
)
type xmlBinding struct{}
func (xmlBinding) Name() string {
return "xml"
}
func (xmlBinding) Bind(req *http.Request, obj interface{}) error {
decoder := xml.NewDecoder(req.Body)
if err := decoder.Decode(obj); err != nil {
return errors.WithStack(err)
}
return validate(obj)
}