├── selector_test.go ├── binding ├── pathparam.go ├── error.go ├── tag_names_test.go ├── body_test.go ├── tidwall_gjson │ └── LICENSE ├── gjson │ ├── internal │ │ ├── caching │ │ │ ├── hashing.go │ │ │ ├── pcache_test.go │ │ │ ├── hashing_test.go │ │ │ ├── fcache.go │ │ │ └── pcache.go │ │ └── rt │ │ │ └── fastvalue.go │ ├── gjson_test.go │ └── gjson.go ├── default.go ├── request.go ├── body.go ├── func.go ├── example_test.go ├── README.md ├── tag_names.go ├── receiver.go ├── bind.go └── param_info.go ├── go.mod ├── .gitignore ├── .github ├── test.sh └── workflows │ └── test.yml ├── validator ├── default.go ├── func.go ├── example_test.go ├── validator.go ├── README.md └── validator_test.go ├── spec_range_test.go ├── tagparser_test.go ├── example_test.go ├── spec_func_test.go ├── go.sum ├── selector.go ├── spec_selector.go ├── handler.go ├── spec_range.go ├── tagparser.go ├── spec_test.go ├── README.md ├── expr_test.go ├── expr.go ├── spec_operator.go ├── spec_func.go ├── spec_operand.go └── LICENSE /selector_test.go: -------------------------------------------------------------------------------- 1 | package tagexpr 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestExprSelector(t *testing.T) { 10 | es := ExprSelector("F1.Index") 11 | field, ok := es.ParentField() 12 | assert.True(t, ok) 13 | assert.Equal(t, "F1", field) 14 | } 15 | -------------------------------------------------------------------------------- /binding/pathparam.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | // PathParams parameter acquisition interface on the URL path 4 | type PathParams interface { 5 | // Get returns the value of the first parameter which key matches the given name. 6 | // If no matching parameter is found, an empty string is returned. 7 | Get(name string) (string, bool) 8 | } 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bytedance/go-tagexpr/v2 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/andeya/ameda v1.5.3 7 | github.com/andeya/goutil v1.0.1 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/nyaruka/phonenumbers v1.0.55 10 | github.com/stretchr/testify v1.7.5 11 | github.com/tidwall/match v1.1.1 12 | github.com/tidwall/pretty v1.2.0 13 | google.golang.org/protobuf v1.27.1 14 | ) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.a 3 | *.so 4 | _obj 5 | _test 6 | *.[568vq] 7 | [568vq].out 8 | *.cgo1.go 9 | *.cgo2.c 10 | _cgo_defun.c 11 | _cgo_gotypes.go 12 | _cgo_export.* 13 | _testmain.go 14 | *.exe 15 | *.exe~ 16 | *.test 17 | *.prof 18 | *.rar 19 | *.zip 20 | *.gz 21 | *.psd 22 | *.bmd 23 | *.cfg 24 | *.pptx 25 | *.log 26 | *.out 27 | *.sublime-project 28 | *.sublime-workspace 29 | .DS_Store 30 | 31 | # idea ignore 32 | .idea/ 33 | 34 | .vscode/ -------------------------------------------------------------------------------- /.github/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # setup env 6 | export GO111MODULE=on 7 | 8 | module_name=$(cat go.mod | grep module | cut -d ' ' -f 2-2) 9 | module_list=(`go list ./...`) 10 | echo "module_name is $module_name" 11 | 12 | echo 'mode: atomic' > coverage.txt 13 | 14 | for ele in "${module_list[@]}"; 15 | do 16 | echo "start handle sub_module: $ele" 17 | go test -covermode=atomic -coverprofile=coverage.tmp -coverpkg=./... -parallel 1 -p 1 -count=1 -gcflags=-l $ele 18 | tail -n +2 coverage.tmp >> coverage.txt || echo "" 19 | rm coverage.tmp || echo "" 20 | done 21 | 22 | # go tool cover -html=coverage.txt -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: go-test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | run: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 30 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.16 21 | 22 | - name: Build 23 | run: | 24 | go build -v ./... 25 | 26 | - name: Test 27 | run: | 28 | ./.github/test.sh 29 | 30 | - name: Upload Cov 31 | run: bash <(curl -s https://codecov.io/bash) 32 | -------------------------------------------------------------------------------- /binding/error.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | // Error validate error 4 | type Error struct { 5 | ErrType, FailField, Msg string 6 | } 7 | 8 | // Error implements error interface. 9 | func (e *Error) Error() string { 10 | if e.Msg != "" { 11 | return e.ErrType + ": expr_path=" + e.FailField + ", cause=" + e.Msg 12 | } 13 | return e.ErrType + ": expr_path=" + e.FailField + ", cause=invalid" 14 | } 15 | 16 | func newDefaultErrorFactory(errType string) func(string, string) error { 17 | return func(failField, msg string) error { 18 | return &Error{ 19 | ErrType: errType, 20 | FailField: failField, 21 | Msg: msg, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /binding/tag_names_test.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDefaultSplitTag(t *testing.T) { 10 | var cases = []struct { 11 | desc string 12 | input string 13 | expected *tagInfo 14 | }{ 15 | { 16 | desc: "default empty", 17 | input: "", 18 | expected: &tagInfo{}, 19 | }, 20 | { 21 | desc: "default", 22 | input: "a,required", 23 | expected: &tagInfo{paramName: "a", required: true}, 24 | }, 25 | } 26 | 27 | for _, c := range cases { 28 | t.Run(c.desc, func(t *testing.T) { 29 | assert.Equal(t, c.expected, newTagInfo(c.input, false)) 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /validator/default.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | var defaultValidator = New("vd").SetErrorFactory(defaultErrorFactory) 4 | 5 | // Default returns the default validator. 6 | // NOTE: 7 | // The tag name is 'vd' 8 | func Default() *Validator { 9 | return defaultValidator 10 | } 11 | 12 | // Validate uses the default validator to validate whether the fields of value is valid. 13 | // NOTE: 14 | // The tag name is 'vd' 15 | // If checkAll=true, validate all the error. 16 | func Validate(value interface{}, checkAll ...bool) error { 17 | return defaultValidator.Validate(value, checkAll...) 18 | } 19 | 20 | // SetErrorFactory customizes the factory of validation error for the default validator. 21 | // NOTE: 22 | // The tag name is 'vd' 23 | func SetErrorFactory(errFactory func(fieldSelector, msg string) error) { 24 | defaultValidator.SetErrorFactory(errFactory) 25 | } 26 | -------------------------------------------------------------------------------- /binding/body_test.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "io/ioutil" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestBody(t *testing.T) { 14 | const USE_GZIP = true 15 | var buf bytes.Buffer 16 | if USE_GZIP { 17 | w := gzip.NewWriter(&buf) 18 | _, err := w.Write([]byte("abc")) 19 | assert.NoError(t, err) 20 | err = w.Flush() 21 | assert.NoError(t, err) 22 | } else { 23 | buf.WriteString("abc") 24 | } 25 | req := &http.Request{ 26 | Body: ioutil.NopCloser(&buf), 27 | } 28 | if USE_GZIP { 29 | req.Header = map[string][]string{ 30 | "Content-Encoding": []string{"gzip"}, 31 | } 32 | } 33 | body, err := GetBody(req) 34 | assert.NoError(t, err) 35 | b, err := ioutil.ReadAll(body) 36 | assert.NoError(t, err) 37 | assert.Equal(t, []byte("abc"), b) 38 | body.Reset() 39 | assert.Equal(t, []byte("abc"), body.bodyBytes) 40 | b, err = ioutil.ReadAll(body) 41 | assert.NoError(t, err) 42 | assert.Equal(t, []byte("abc"), b) 43 | body.Close() 44 | assert.Equal(t, []byte(nil), body.Bytes()) 45 | } 46 | -------------------------------------------------------------------------------- /binding/tidwall_gjson/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Josh Baker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /binding/gjson/internal/caching/hashing.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ByteDance Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package caching 18 | 19 | import ( 20 | "unsafe" 21 | 22 | "github.com/bytedance/go-tagexpr/v2/binding/gjson/internal/rt" 23 | ) 24 | 25 | var ( 26 | V_strhash = rt.UnpackEface(strhash) 27 | S_strhash = *(*uintptr)(V_strhash.Value) 28 | ) 29 | 30 | //go:noescape 31 | //go:linkname strhash runtime.strhash 32 | func strhash(_ unsafe.Pointer, _ uintptr) uintptr 33 | 34 | func StrHash(s string) uint64 { 35 | if v := strhash(unsafe.Pointer(&s), 0); v == 0 { 36 | return 1 37 | } else { 38 | return uint64(v) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spec_range_test.go: -------------------------------------------------------------------------------- 1 | package tagexpr_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bytedance/go-tagexpr/v2" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestIssue12(t *testing.T) { 11 | var vm = tagexpr.New("te") 12 | type I int 13 | type S struct { 14 | F []I `te:"range($, '>'+sprintf('%v:%v', #k, #v+2+len($)))"` 15 | Fs [][]I `te:"range($, range(#v, '>'+sprintf('%v:%v', #k, #v+2+##)))"` 16 | M map[string]I `te:"range($, '>'+sprintf('%s:%v', #k, #v+2+##))"` 17 | MFs []map[string][]I `te:"range($, range(#v, range(#v, '>'+sprintf('%v:%v', #k, #v+2+##))))"` 18 | MFs2 []map[string][]I `te:"range($, range(#v, range(#v, '>'+sprintf('%v:%v', #k, #v+2+##))))"` 19 | } 20 | a := []I{2, 3} 21 | r := vm.MustRun(S{ 22 | F: a, 23 | Fs: [][]I{a}, 24 | M: map[string]I{"m0": 2, "m1": 3}, 25 | MFs: []map[string][]I{{"m": a}}, 26 | MFs2: []map[string][]I{}, 27 | }) 28 | assert.Equal(t, []interface{}{">0:6", ">1:7"}, r.Eval("F")) 29 | assert.Equal(t, []interface{}{[]interface{}{">0:6", ">1:7"}}, r.Eval("Fs")) 30 | assert.Equal(t, []interface{}{">m0:6", ">m1:7"}, r.Eval("M")) 31 | assert.Equal(t, []interface{}{[]interface{}{[]interface{}{">0:6", ">1:7"}}}, r.Eval("MFs")) 32 | assert.Equal(t, []interface{}{}, r.Eval("MFs2")) 33 | assert.Equal(t, true, r.EvalBool("MFs2")) 34 | } 35 | -------------------------------------------------------------------------------- /binding/gjson/internal/caching/pcache_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ByteDance Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package caching 18 | 19 | import ( 20 | "sync" 21 | "testing" 22 | 23 | "github.com/bytedance/go-tagexpr/v2/binding/gjson/internal/rt" 24 | ) 25 | 26 | func TestPcacheRace(t *testing.T) { 27 | t.Parallel() 28 | 29 | pc := CreateProgramCache() 30 | wg := sync.WaitGroup{} 31 | wg.Add(2) 32 | start := make(chan struct{}, 2) 33 | 34 | go func() { 35 | defer wg.Done() 36 | var k = map[string]interface{}{} 37 | <-start 38 | for i := 0; i < 100; i++ { 39 | _, _ = pc.Compute(rt.UnpackEface(k).Type, func(_ *rt.GoType) (interface{}, error) { 40 | return map[string]interface{}{}, nil 41 | }) 42 | } 43 | }() 44 | 45 | go func() { 46 | defer wg.Done() 47 | var k = map[string]interface{}{} 48 | <-start 49 | for i := 0; i < 100; i++ { 50 | v := pc.Get(rt.UnpackEface(k).Type) 51 | t.Log(k, v) 52 | } 53 | }() 54 | 55 | start <- struct{}{} 56 | start <- struct{}{} 57 | wg.Wait() 58 | } 59 | -------------------------------------------------------------------------------- /binding/default.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import "net/http" 4 | 5 | var defaultBinding = New(nil) 6 | 7 | // Default returns the default binding. 8 | // NOTE: 9 | // path tag name is 'path'; 10 | // query tag name is 'query'; 11 | // header tag name is 'header'; 12 | // cookie tag name is 'cookie'; 13 | // raw_body tag name is 'raw_body'; 14 | // form tag name is 'form'; 15 | // validator tag name is 'vd'; 16 | // protobuf tag name is 'protobuf'; 17 | // json tag name is 'json'; 18 | // LooseZeroMode is false. 19 | func Default() *Binding { 20 | return defaultBinding 21 | } 22 | 23 | // SetLooseZeroMode if set to true, 24 | // the empty string request parameter is bound to the zero value of parameter. 25 | // NOTE: 26 | // The default is false; 27 | // Suitable for these parameter types: query/header/cookie/form . 28 | func SetLooseZeroMode(enable bool) { 29 | defaultBinding.SetLooseZeroMode(enable) 30 | } 31 | 32 | // SetErrorFactory customizes the factory of validation error. 33 | // NOTE: 34 | // If errFactory==nil, the default is used 35 | func SetErrorFactory(bindErrFactory, validatingErrFactory func(failField, msg string) error) { 36 | defaultBinding.SetErrorFactory(bindErrFactory, validatingErrFactory) 37 | } 38 | 39 | // BindAndValidate binds the request parameters and validates them if needed. 40 | func BindAndValidate(structPointer interface{}, req *http.Request, pathParams PathParams) error { 41 | return defaultBinding.BindAndValidate(structPointer, req, pathParams) 42 | } 43 | 44 | // Bind binds the request parameters. 45 | func Bind(structPointer interface{}, req *http.Request, pathParams PathParams) error { 46 | return defaultBinding.Bind(structPointer, req, pathParams) 47 | } 48 | 49 | // Validate validates whether the fields of value is valid. 50 | func Validate(value interface{}) error { 51 | return defaultBinding.Validate(value) 52 | } 53 | -------------------------------------------------------------------------------- /tagparser_test.go: -------------------------------------------------------------------------------- 1 | package tagexpr 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTagparser(t *testing.T) { 11 | cases := []struct { 12 | tag reflect.StructTag 13 | expect map[string]string 14 | fail bool 15 | }{ 16 | { 17 | tag: `tagexpr:"$>0"`, 18 | expect: map[string]string{ 19 | "@": "$>0", 20 | }, 21 | }, { 22 | tag: `tagexpr:"$>0;'xxx'"`, 23 | fail: true, 24 | }, { 25 | tag: `tagexpr:"$>0;b:sprintf('%[1]T; %[1]v',(X)$)"`, 26 | expect: map[string]string{ 27 | "@": `$>0`, 28 | "b": `sprintf('%[1]T; %[1]v',(X)$)`, 29 | }, 30 | }, { 31 | tag: `tagexpr:"a:$=='0;1;';b:sprintf('%[1]T; %[1]v',(X)$)"`, 32 | expect: map[string]string{ 33 | "a": `$=='0;1;'`, 34 | "b": `sprintf('%[1]T; %[1]v',(X)$)`, 35 | }, 36 | }, { 37 | tag: `tagexpr:"a:1;;b:2"`, 38 | expect: map[string]string{ 39 | "a": `1`, 40 | "b": `2`, 41 | }, 42 | }, { 43 | tag: `tagexpr:";a:1;;b:2;;;"`, 44 | expect: map[string]string{ 45 | "a": `1`, 46 | "b": `2`, 47 | }, 48 | }, { 49 | tag: `tagexpr:";a:'123\\'';;b:'1\\'23';c:'1\\'2\\'3';;"`, 50 | expect: map[string]string{ 51 | "a": `'123\''`, 52 | "b": `'1\'23'`, 53 | "c": `'1\'2\'3'`, 54 | }, 55 | }, { 56 | tag: `tagexpr:"email($)"`, 57 | expect: map[string]string{ 58 | "@": `email($)`, 59 | }, 60 | }, { 61 | tag: `tagexpr:"false"`, 62 | expect: map[string]string{ 63 | "@": `false`, 64 | }, 65 | }, 66 | } 67 | 68 | for _, c := range cases { 69 | r, e := parseTag(c.tag.Get("tagexpr")) 70 | if e != nil == c.fail { 71 | assert.Equal(t, c.expect, r, c.tag) 72 | } else { 73 | assert.Failf(t, string(c.tag), "kvs:%v, err:%v", r, e) 74 | } 75 | if e != nil { 76 | t.Logf("tag:%q, errMsg:%v", c.tag, e) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /binding/gjson/internal/caching/hashing_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ByteDance Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package caching 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | "unsafe" 23 | 24 | "github.com/bytedance/go-tagexpr/v2/binding/gjson/internal/rt" 25 | ) 26 | 27 | const ( 28 | _H_basis uint64 = 0xcbf29ce484222325 29 | _H_prime uint64 = 0x00000100000001b3 30 | ) 31 | 32 | func fnv1a(s string) uint64 { 33 | v := _H_basis 34 | m := (*rt.GoString)(unsafe.Pointer(&s)) 35 | 36 | /* hash each byte */ 37 | for i := 0; i < m.Len; i++ { 38 | v ^= uint64(*(*uint8)(unsafe.Pointer(uintptr(m.Ptr) + uintptr(i)))) 39 | v *= _H_prime 40 | } 41 | 42 | /* never returns 0 for hash */ 43 | if v == 0 { 44 | return 1 45 | } else { 46 | return v 47 | } 48 | } 49 | 50 | func TestHashing_Fnv1a(t *testing.T) { 51 | fmt.Printf("%#x\n", fnv1a("hello, world")) 52 | } 53 | 54 | func TestHashing_StrHash(t *testing.T) { 55 | s := "hello, world" 56 | fmt.Printf("%#x\n", StrHash(s)) 57 | } 58 | 59 | var fn_fnv1a = fnv1a 60 | 61 | func BenchmarkHashing_Fnv1a(b *testing.B) { 62 | for i := 0; i < b.N; i++ { 63 | fn_fnv1a("accountid_interval_aweme_second") 64 | } 65 | } 66 | 67 | func BenchmarkHashing_StrHash(b *testing.B) { 68 | for i := 0; i < b.N; i++ { 69 | StrHash("accountid_interval_aweme_second") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Bytedance Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tagexpr_test 16 | 17 | import ( 18 | "fmt" 19 | 20 | tagexpr "github.com/bytedance/go-tagexpr/v2" 21 | ) 22 | 23 | func Example() { 24 | type T struct { 25 | A int `tagexpr:"$<0||$>=100"` 26 | B string `tagexpr:"len($)>1 && regexp('^\\w*$')"` 27 | C bool `tagexpr:"expr1:(f.g)$>0 && $; expr2:'C must be true when T.f.g>0'"` 28 | d []string `tagexpr:"@:len($)>0 && $[0]=='D'; msg:sprintf('invalid d: %v',$)"` 29 | e map[string]int `tagexpr:"len($)==$['len']"` 30 | e2 map[string]*int `tagexpr:"len($)==$['len']"` 31 | f struct { 32 | g int `tagexpr:"$"` 33 | } 34 | h int `tagexpr:"$>minVal"` 35 | } 36 | 37 | vm := tagexpr.New("tagexpr") 38 | t := &T{ 39 | A: 107, 40 | B: "abc", 41 | C: true, 42 | d: []string{"x", "y"}, 43 | e: map[string]int{"len": 1}, 44 | e2: map[string]*int{"len": new(int)}, 45 | f: struct { 46 | g int `tagexpr:"$"` 47 | }{1}, 48 | h: 10, 49 | } 50 | 51 | tagExpr, err := vm.Run(t) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | fmt.Println(tagExpr.Eval("A")) 57 | fmt.Println(tagExpr.Eval("B")) 58 | fmt.Println(tagExpr.Eval("C@expr1")) 59 | fmt.Println(tagExpr.Eval("C@expr2")) 60 | if !tagExpr.Eval("d").(bool) { 61 | fmt.Println(tagExpr.Eval("d@msg")) 62 | } 63 | fmt.Println(tagExpr.Eval("e")) 64 | fmt.Println(tagExpr.Eval("e2")) 65 | fmt.Println(tagExpr.Eval("f.g")) 66 | fmt.Println(tagExpr.EvalWithEnv("h", map[string]interface{}{"minVal": 9})) 67 | fmt.Println(tagExpr.EvalWithEnv("h", map[string]interface{}{"minVal": 11})) 68 | 69 | // Output: 70 | // true 71 | // true 72 | // true 73 | // C must be true when T.f.g>0 74 | // invalid d: [x y] 75 | // true 76 | // false 77 | // 1 78 | // true 79 | // false 80 | } 81 | -------------------------------------------------------------------------------- /binding/request.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "mime/multipart" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | type requestWithFileHeader interface { 12 | Request 13 | GetFileHeaders() (map[string][]*multipart.FileHeader, error) 14 | } 15 | 16 | type Request interface { 17 | GetMethod() string 18 | GetQuery() url.Values 19 | GetContentType() string 20 | GetHeader() http.Header 21 | GetCookies() []*http.Cookie 22 | GetBody() ([]byte, error) 23 | GetPostForm() (url.Values, error) 24 | GetForm() (url.Values, error) 25 | } 26 | 27 | func wrapRequest(req *http.Request) Request { 28 | r := &httpRequest{ 29 | Request: req, 30 | } 31 | if getBodyCodec(r) == bodyForm && req.PostForm == nil { 32 | b, _ := r.GetBody() 33 | if b != nil { 34 | req.ParseMultipartForm(defaultMaxMemory) 35 | } 36 | } 37 | return r 38 | } 39 | 40 | type httpRequest struct { 41 | *http.Request 42 | } 43 | 44 | func (r *httpRequest) GetMethod() string { 45 | return r.Method 46 | } 47 | func (r *httpRequest) GetQuery() url.Values { 48 | return r.URL.Query() 49 | } 50 | 51 | func (r *httpRequest) GetContentType() string { 52 | return r.GetHeader().Get("Content-Type") 53 | } 54 | 55 | func (r *httpRequest) GetHeader() http.Header { 56 | return r.Header 57 | } 58 | 59 | func (r *httpRequest) GetCookies() []*http.Cookie { 60 | return r.Cookies() 61 | } 62 | 63 | func (r *httpRequest) GetBody() ([]byte, error) { 64 | body, _ := r.Body.(*Body) 65 | if body != nil { 66 | body.Reset() 67 | return body.bodyBytes, nil 68 | } 69 | switch r.Method { 70 | case "POST", "PUT", "PATCH", "DELETE": 71 | var buf bytes.Buffer 72 | _, err := io.Copy(&buf, r.Body) 73 | r.Body.Close() 74 | if err != nil { 75 | return nil, err 76 | } 77 | body = &Body{ 78 | Buffer: &buf, 79 | bodyBytes: buf.Bytes(), 80 | } 81 | r.Body = body 82 | return body.bodyBytes, nil 83 | default: 84 | return nil, nil 85 | } 86 | } 87 | 88 | func (r *httpRequest) GetPostForm() (url.Values, error) { 89 | return r.PostForm, nil 90 | } 91 | 92 | func (r *httpRequest) GetForm() (url.Values, error) { 93 | return r.Form, nil 94 | } 95 | 96 | func (r *httpRequest) GetFileHeaders() (map[string][]*multipart.FileHeader, error) { 97 | if r.MultipartForm == nil { 98 | return nil, nil 99 | } 100 | return r.MultipartForm.File, nil 101 | } 102 | -------------------------------------------------------------------------------- /spec_func_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Bytedance Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tagexpr_test 16 | 17 | import ( 18 | "regexp" 19 | "testing" 20 | 21 | "github.com/bytedance/go-tagexpr/v2" 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestFunc(t *testing.T) { 26 | var emailRegexp = regexp.MustCompile( 27 | "^([A-Za-z0-9_\\-\\.\u4e00-\u9fa5])+\\@([A-Za-z0-9_\\-\\.])+\\.([A-Za-z]{2,8})$", 28 | ) 29 | tagexpr.RegFunc("email", func(args ...interface{}) interface{} { 30 | if len(args) == 0 { 31 | return false 32 | } 33 | s, ok := args[0].(string) 34 | if !ok { 35 | return false 36 | } 37 | t.Log(s) 38 | return emailRegexp.MatchString(s) 39 | }) 40 | 41 | var vm = tagexpr.New("te") 42 | 43 | type T struct { 44 | Email string `te:"email($)"` 45 | } 46 | var cases = []struct { 47 | email string 48 | expect bool 49 | }{ 50 | {"", false}, 51 | {"henrylee2cn@gmail.com", true}, 52 | } 53 | 54 | obj := new(T) 55 | for _, c := range cases { 56 | obj.Email = c.email 57 | te := vm.MustRun(obj) 58 | got := te.EvalBool("Email") 59 | if got != c.expect { 60 | t.Fatalf("email: %s, expect: %v, but got: %v", c.email, c.expect, got) 61 | } 62 | } 63 | 64 | // test len 65 | type R struct { 66 | Str string `vd:"mblen($)<6"` 67 | } 68 | var lenCases = []struct { 69 | str string 70 | expect bool 71 | }{ 72 | {"123", true}, 73 | {"一二三四五六七", false}, 74 | {"一二三四五", true}, 75 | } 76 | 77 | lenObj := new(R) 78 | vm = tagexpr.New("vd") 79 | for _, lenCase := range lenCases { 80 | lenObj.Str = lenCase.str 81 | te := vm.MustRun(lenObj) 82 | got := te.EvalBool("Str") 83 | if got != lenCase.expect { 84 | t.Fatalf("string: %v, expect: %v, but got: %v", lenCase.str, lenCase.expect, got) 85 | } 86 | } 87 | } 88 | 89 | func TestRangeIn(t *testing.T) { 90 | var vm = tagexpr.New("te") 91 | type S struct { 92 | F []string `te:"range($, in(#v, '', 'ttp', 'euttp'))"` 93 | } 94 | a := []string{"ttp", "", "euttp"} 95 | r := vm.MustRun(S{ 96 | F: a, 97 | // F: b, 98 | }) 99 | assert.Equal(t, []interface{}{true, true, true}, r.Eval("F")) 100 | } 101 | -------------------------------------------------------------------------------- /binding/body.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "bytes" 5 | "compress/flate" 6 | "compress/gzip" 7 | "compress/zlib" 8 | "errors" 9 | "io" 10 | "net/http" 11 | "strings" 12 | 13 | "google.golang.org/protobuf/proto" 14 | ) 15 | 16 | func getBodyCodec(req Request) codec { 17 | // according to rfc7231 https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5 18 | // content type just for payload and payload for Http GET Method are meanless; this 19 | // will cause bad case for http GET method with a muanual added Content-Type Header 20 | if req.GetMethod() == http.MethodGet { 21 | return bodyUnsupport 22 | } 23 | ct := req.GetContentType() 24 | idx := strings.Index(ct, ";") 25 | if idx != -1 { 26 | ct = strings.TrimRight(ct[:idx], " ") 27 | } 28 | switch ct { 29 | case "application/json": 30 | return bodyJSON 31 | case "application/x-protobuf": 32 | return bodyProtobuf 33 | case "application/x-www-form-urlencoded", "multipart/form-data": 34 | return bodyForm 35 | default: 36 | return bodyUnsupport 37 | } 38 | } 39 | 40 | // Body body copy 41 | type Body struct { 42 | *bytes.Buffer 43 | bodyBytes []byte 44 | } 45 | 46 | // Close close. 47 | func (b *Body) Close() error { 48 | b.Buffer = nil 49 | b.bodyBytes = nil 50 | return nil 51 | } 52 | 53 | // Reset zero offset. 54 | func (b *Body) Reset() { 55 | b.Buffer = bytes.NewBuffer(b.bodyBytes) 56 | } 57 | 58 | // Bytes returns all of the body bytes. 59 | func (b *Body) Bytes() []byte { 60 | return b.bodyBytes 61 | } 62 | 63 | // Len returns all of the body length. 64 | func (b *Body) Len() int { 65 | return len(b.bodyBytes) 66 | } 67 | 68 | // GetBody get the body from http.Request 69 | func GetBody(r *http.Request) (*Body, error) { 70 | switch body := r.Body.(type) { 71 | case *Body: 72 | body.Reset() 73 | return body, nil 74 | default: 75 | var err error 76 | var closeBody = r.Body.Close 77 | switch r.Header.Get("Content-Encoding") { 78 | case "gzip": 79 | var gzipReader *gzip.Reader 80 | gzipReader, err = gzip.NewReader(r.Body) 81 | if err == nil { 82 | r.Body = gzipReader 83 | } 84 | case "deflate": 85 | r.Body = flate.NewReader(r.Body) 86 | case "zlib": 87 | var readCloser io.ReadCloser 88 | readCloser, err = zlib.NewReader(r.Body) 89 | if err == nil { 90 | r.Body = readCloser 91 | } 92 | } 93 | if err != nil { 94 | return nil, err 95 | } 96 | var buf bytes.Buffer 97 | _, _ = io.Copy(&buf, r.Body) 98 | _ = closeBody() 99 | _body := &Body{ 100 | Buffer: &buf, 101 | bodyBytes: buf.Bytes(), 102 | } 103 | r.Body = _body 104 | return _body, nil 105 | } 106 | } 107 | 108 | func bindProtobuf(pointer interface{}, bodyBytes []byte) error { 109 | msg, ok := pointer.(proto.Message) 110 | if !ok { 111 | return errors.New("protobuf content type is not supported") 112 | } 113 | return proto.Unmarshal(bodyBytes, msg) 114 | } 115 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andeya/ameda v1.5.3 h1:SvqnhQPZwwabS8HQTRGfJwWPl2w9ZIPInHAw9aE1Wlk= 2 | github.com/andeya/ameda v1.5.3/go.mod h1:FQDHRe1I995v6GG+8aJ7UIUToEmbdTJn/U26NCPIgXQ= 3 | github.com/andeya/goutil v1.0.1 h1:eiYwVyAnnK0dXU5FJsNjExkJW4exUGn/xefPt3k4eXg= 4 | github.com/andeya/goutil v1.0.1/go.mod h1:jEG5/QnnhG7yGxwFUX6Q+JGMif7sjdHmmNVjn7nhJDo= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 9 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= 10 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 11 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 12 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 13 | github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= 14 | github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 19 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= 21 | github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 22 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 23 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 24 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 25 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 26 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 27 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 28 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 29 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 30 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /selector.go: -------------------------------------------------------------------------------- 1 | package tagexpr 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const ( 8 | // FieldSeparator in the expression selector, 9 | // the separator between field names 10 | FieldSeparator = "." 11 | // ExprNameSeparator in the expression selector, 12 | // the separator of the field name and expression name 13 | ExprNameSeparator = "@" 14 | // DefaultExprName the default name of single model expression 15 | DefaultExprName = ExprNameSeparator 16 | ) 17 | 18 | // FieldSelector expression selector 19 | type FieldSelector string 20 | 21 | // JoinFieldSelector creates a field selector. 22 | func JoinFieldSelector(path ...string) string { 23 | return strings.Join(path, FieldSeparator) 24 | } 25 | 26 | // Name returns the current field name. 27 | func (f FieldSelector) Name() string { 28 | s := string(f) 29 | idx := strings.LastIndex(s, FieldSeparator) 30 | if idx == -1 { 31 | return s 32 | } 33 | return s[idx+1:] 34 | } 35 | 36 | // Split returns the path segments and the current field name. 37 | func (f FieldSelector) Split() (paths []string, name string) { 38 | s := string(f) 39 | a := strings.Split(s, FieldSeparator) 40 | idx := len(a) - 1 41 | if idx > 0 { 42 | return a[:idx], a[idx] 43 | } 44 | return nil, s 45 | } 46 | 47 | // Parent returns the parent FieldSelector. 48 | func (f FieldSelector) Parent() (string, bool) { 49 | s := string(f) 50 | i := strings.LastIndex(s, FieldSeparator) 51 | if i < 0 { 52 | return "", false 53 | } 54 | return s[:i], true 55 | } 56 | 57 | // String returns string type value. 58 | func (f FieldSelector) String() string { 59 | return string(f) 60 | } 61 | 62 | // JoinExprSelector creates a expression selector. 63 | func JoinExprSelector(pathFields []string, exprName string) string { 64 | p := strings.Join(pathFields, FieldSeparator) 65 | if p == "" || exprName == "" { 66 | return p 67 | } 68 | return p + ExprNameSeparator + exprName 69 | } 70 | 71 | // ExprSelector expression selector 72 | type ExprSelector string 73 | 74 | // Name returns the name of the expression. 75 | func (e ExprSelector) Name() string { 76 | s := string(e) 77 | atIdx := strings.LastIndex(s, ExprNameSeparator) 78 | if atIdx == -1 { 79 | return DefaultExprName 80 | } 81 | return s[atIdx+1:] 82 | } 83 | 84 | // Field returns the field selector it belongs to. 85 | func (e ExprSelector) Field() string { 86 | s := string(e) 87 | idx := strings.LastIndex(s, ExprNameSeparator) 88 | if idx != -1 { 89 | s = s[:idx] 90 | } 91 | return s 92 | } 93 | 94 | // ParentField returns the parent field selector it belongs to. 95 | func (e ExprSelector) ParentField() (string, bool) { 96 | return FieldSelector(e.Field()).Parent() 97 | } 98 | 99 | // Split returns the field selector and the expression name. 100 | func (e ExprSelector) Split() (field FieldSelector, name string) { 101 | s := string(e) 102 | atIdx := strings.LastIndex(s, ExprNameSeparator) 103 | if atIdx == -1 { 104 | return FieldSelector(s), DefaultExprName 105 | } 106 | return FieldSelector(s[:atIdx]), s[atIdx+1:] 107 | } 108 | 109 | // String returns string type value. 110 | func (e ExprSelector) String() string { 111 | return string(e) 112 | } 113 | -------------------------------------------------------------------------------- /validator/func.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | 7 | "github.com/nyaruka/phonenumbers" 8 | 9 | "github.com/bytedance/go-tagexpr/v2" 10 | ) 11 | 12 | // ErrInvalidWithoutMsg verification error without error message. 13 | var ErrInvalidWithoutMsg = errors.New("") 14 | 15 | // MustRegFunc registers validator function expression. 16 | // NOTE: 17 | // 18 | // panic if exist error; 19 | // example: phone($) or phone($,'CN'); 20 | // If @force=true, allow to cover the existed same @funcName; 21 | // The go number types always are float64; 22 | // The go string types always are string. 23 | func MustRegFunc(funcName string, fn func(args ...interface{}) error, force ...bool) { 24 | err := RegFunc(funcName, fn, force...) 25 | if err != nil { 26 | panic(err) 27 | } 28 | } 29 | 30 | // RegFunc registers validator function expression. 31 | // NOTE: 32 | // 33 | // example: phone($) or phone($,'CN'); 34 | // If @force=true, allow to cover the existed same @funcName; 35 | // The go number types always are float64; 36 | // The go string types always are string. 37 | func RegFunc(funcName string, fn func(args ...interface{}) error, force ...bool) error { 38 | return tagexpr.RegFunc(funcName, func(args ...interface{}) interface{} { 39 | err := fn(args...) 40 | if err == nil { 41 | // nil defaults to false, so returns true 42 | return true 43 | } 44 | return err 45 | }, force...) 46 | } 47 | 48 | func init() { 49 | var pattern = "^([A-Za-z0-9_\\-\\.\u4e00-\u9fa5])+\\@([A-Za-z0-9_\\-\\.])+\\.([A-Za-z]{2,8})$" 50 | emailRegexp := regexp.MustCompile(pattern) 51 | MustRegFunc("email", func(args ...interface{}) error { 52 | if len(args) != 1 { 53 | return errors.New("number of parameters of email function is not one") 54 | } 55 | s, ok := args[0].(string) 56 | if !ok { 57 | return errors.New("parameter of email function is not string type") 58 | } 59 | matched := emailRegexp.MatchString(s) 60 | if !matched { 61 | // return ErrInvalidWithoutMsg 62 | return errors.New("email format is incorrect") 63 | } 64 | return nil 65 | }, true) 66 | } 67 | 68 | func init() { 69 | // phone: defaultRegion is 'CN' 70 | MustRegFunc("phone", func(args ...interface{}) error { 71 | var numberToParse, defaultRegion string 72 | var ok bool 73 | switch len(args) { 74 | default: 75 | return errors.New("the number of parameters of phone function is not one or two") 76 | case 2: 77 | defaultRegion, ok = args[1].(string) 78 | if !ok { 79 | return errors.New("the 2nd parameter of phone function is not string type") 80 | } 81 | fallthrough 82 | case 1: 83 | numberToParse, ok = args[0].(string) 84 | if !ok { 85 | return errors.New("the 1st parameter of phone function is not string type") 86 | } 87 | } 88 | if defaultRegion == "" { 89 | defaultRegion = "CN" 90 | } 91 | num, err := phonenumbers.Parse(numberToParse, defaultRegion) 92 | if err != nil { 93 | return err 94 | } 95 | matched := phonenumbers.IsValidNumber(num) 96 | if !matched { 97 | // return ErrInvalidWithoutMsg 98 | return errors.New("phone format is incorrect") 99 | } 100 | return nil 101 | }, true) 102 | } 103 | -------------------------------------------------------------------------------- /binding/gjson/internal/caching/fcache.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ByteDance Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package caching 18 | 19 | import ( 20 | "strings" 21 | "unsafe" 22 | 23 | "github.com/bytedance/go-tagexpr/v2/binding/gjson/internal/rt" 24 | ) 25 | 26 | type FieldMap struct { 27 | N uint64 28 | b unsafe.Pointer 29 | m map[string]int 30 | } 31 | 32 | type FieldEntry struct { 33 | ID int 34 | Name string 35 | Hash uint64 36 | } 37 | 38 | const ( 39 | FieldMap_N = int64(unsafe.Offsetof(FieldMap{}.N)) 40 | FieldMap_b = int64(unsafe.Offsetof(FieldMap{}.b)) 41 | FieldEntrySize = int64(unsafe.Sizeof(FieldEntry{})) 42 | ) 43 | 44 | func newBucket(n int) unsafe.Pointer { 45 | v := make([]FieldEntry, n) 46 | return (*rt.GoSlice)(unsafe.Pointer(&v)).Ptr 47 | } 48 | 49 | func CreateFieldMap(n int) *FieldMap { 50 | return &FieldMap{ 51 | N: uint64(n * 2), 52 | b: newBucket(n * 2), // LoadFactor = 0.5 53 | m: make(map[string]int, n*2), 54 | } 55 | } 56 | 57 | func (self *FieldMap) At(p uint64) *FieldEntry { 58 | off := uintptr(p) * uintptr(FieldEntrySize) 59 | return (*FieldEntry)(unsafe.Pointer(uintptr(self.b) + off)) 60 | } 61 | 62 | // Get searches FieldMap by name. JIT generated assembly does NOT call this 63 | // function, rather it implements its own version directly in assembly. So 64 | // we must ensure this function stays in sync with the JIT generated one. 65 | func (self *FieldMap) Get(name string) int { 66 | h := StrHash(name) 67 | p := h % self.N 68 | s := self.At(p) 69 | 70 | /* find the element; 71 | * the hash map is never full, so the loop will always terminate */ 72 | for s.Hash != 0 { 73 | if s.Hash == h && s.Name == name { 74 | return s.ID 75 | } else { 76 | p = (p + 1) % self.N 77 | s = self.At(p) 78 | } 79 | } 80 | 81 | /* not found */ 82 | return -1 83 | } 84 | 85 | func (self *FieldMap) Set(name string, i int) { 86 | h := StrHash(name) 87 | p := h % self.N 88 | s := self.At(p) 89 | 90 | /* searching for an empty slot; 91 | * the hash map is never full, so the loop will always terminate */ 92 | for s.Hash != 0 { 93 | p = (p + 1) % self.N 94 | s = self.At(p) 95 | } 96 | 97 | /* set the value */ 98 | s.ID = i 99 | s.Hash = h 100 | s.Name = name 101 | 102 | /* add the case-insensitive version, prefer the one with smaller field ID */ 103 | key := strings.ToLower(name) 104 | if v, ok := self.m[key]; !ok || i < v { 105 | self.m[key] = i 106 | } 107 | } 108 | 109 | func (self *FieldMap) GetCaseInsensitive(name string) int { 110 | if i, ok := self.m[strings.ToLower(name)]; ok { 111 | return i 112 | } else { 113 | return -1 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /spec_selector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Bytedance Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tagexpr 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "regexp" 21 | "strings" 22 | ) 23 | 24 | type selectorExprNode struct { 25 | exprBackground 26 | field, name string 27 | subExprs []ExprNode 28 | boolOpposite *bool 29 | signOpposite *bool 30 | } 31 | 32 | func (se *selectorExprNode) String() string { 33 | return fmt.Sprintf("(%s)%s", se.field, se.name) 34 | } 35 | 36 | func (p *Expr) readSelectorExprNode(expr *string) ExprNode { 37 | field, name, subSelector, boolOpposite, signOpposite, found := findSelector(expr) 38 | if !found { 39 | return nil 40 | } 41 | operand := &selectorExprNode{ 42 | field: field, 43 | name: name, 44 | boolOpposite: boolOpposite, 45 | signOpposite: signOpposite, 46 | } 47 | operand.subExprs = make([]ExprNode, 0, len(subSelector)) 48 | for _, s := range subSelector { 49 | grp := newGroupExprNode() 50 | err := p.parseExprNode(&s, grp) 51 | if err != nil { 52 | return nil 53 | } 54 | sortPriority(grp) 55 | operand.subExprs = append(operand.subExprs, grp) 56 | } 57 | return operand 58 | } 59 | 60 | var selectorRegexp = regexp.MustCompile(`^([\!\+\-]*)(\([ \t]*[A-Za-z_]+[A-Za-z0-9_\.]*[ \t]*\))?(\$)([\)\[\],\+\-\*\/%><\|&!=\^ \t\\]|$)`) 61 | 62 | func findSelector(expr *string) (field string, name string, subSelector []string, boolOpposite, signOpposite *bool, found bool) { 63 | raw := *expr 64 | a := selectorRegexp.FindAllStringSubmatch(raw, -1) 65 | if len(a) != 1 { 66 | return 67 | } 68 | r := a[0] 69 | if s0 := r[2]; len(s0) > 0 { 70 | field = strings.TrimSpace(s0[1 : len(s0)-1]) 71 | } 72 | name = r[3] 73 | *expr = (*expr)[len(a[0][0])-len(r[4]):] 74 | for { 75 | sub := readPairedSymbol(expr, '[', ']') 76 | if sub == nil { 77 | break 78 | } 79 | if *sub == "" || (*sub)[0] == '[' { 80 | *expr = raw 81 | return "", "", nil, nil, nil, false 82 | } 83 | subSelector = append(subSelector, strings.TrimSpace(*sub)) 84 | } 85 | prefix := r[1] 86 | if len(prefix) == 0 { 87 | found = true 88 | return 89 | } 90 | _, boolOpposite, signOpposite = getBoolAndSignOpposite(&prefix) 91 | found = true 92 | return 93 | } 94 | 95 | func (se *selectorExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 96 | var subFields []interface{} 97 | if n := len(se.subExprs); n > 0 { 98 | subFields = make([]interface{}, n) 99 | for i, e := range se.subExprs { 100 | subFields[i] = e.Run(ctx, currField, tagExpr) 101 | } 102 | } 103 | field := se.field 104 | if field == "" { 105 | field = currField 106 | } 107 | v := tagExpr.getValue(field, subFields) 108 | return realValue(v, se.boolOpposite, se.signOpposite) 109 | } 110 | -------------------------------------------------------------------------------- /validator/example_test.go: -------------------------------------------------------------------------------- 1 | package validator_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | vd "github.com/bytedance/go-tagexpr/v2/validator" 7 | ) 8 | 9 | func Example() { 10 | type InfoRequest struct { 11 | Name string `vd:"($!='Alice'||(Age)$==18) && regexp('\\w')"` 12 | Age int `vd:"$>0"` 13 | Email string `vd:"email($)"` 14 | Phone1 string `vd:"phone($)"` 15 | OtherPhones []string `vd:"range($, phone(#v,'CN'))"` 16 | *InfoRequest `vd:"?"` 17 | Info1 *InfoRequest `vd:"?"` 18 | Info2 *InfoRequest `vd:"-"` 19 | } 20 | info := &InfoRequest{ 21 | Name: "Alice", 22 | Age: 18, 23 | Email: "henrylee2cn@gmail.com", 24 | Phone1: "+8618812345678", 25 | OtherPhones: []string{"18812345679", "18812345680"}, 26 | } 27 | fmt.Println(vd.Validate(info)) 28 | 29 | type A struct { 30 | A int `vd:"$<0||$>=100"` 31 | Info interface{} 32 | } 33 | info.Email = "xxx" 34 | a := &A{A: 107, Info: info} 35 | fmt.Println(vd.Validate(a)) 36 | type B struct { 37 | B string `vd:"len($)>1 && regexp('^\\w*$')"` 38 | } 39 | b := &B{"abc"} 40 | fmt.Println(vd.Validate(b) == nil) 41 | 42 | type C struct { 43 | C bool `vd:"@:(S.A)$>0 && !$; msg:'C must be false when S.A>0'"` 44 | S *A 45 | } 46 | c := &C{C: true, S: a} 47 | fmt.Println(vd.Validate(c)) 48 | 49 | type D struct { 50 | d []string `vd:"@:len($)>0 && $[0]=='D'; msg:sprintf('invalid d: %v',$)"` 51 | } 52 | d := &D{d: []string{"x", "y"}} 53 | fmt.Println(vd.Validate(d)) 54 | 55 | type E struct { 56 | e map[string]int `vd:"len($)==$['len']"` 57 | } 58 | e := &E{map[string]int{"len": 2}} 59 | fmt.Println(vd.Validate(e)) 60 | 61 | // Customizes the factory of validation error. 62 | vd.SetErrorFactory(func(failPath, msg string) error { 63 | return fmt.Errorf(`{"succ":false, "error":"validation failed: %s"}`, failPath) 64 | }) 65 | 66 | type F struct { 67 | f struct { 68 | g int `vd:"$%3==0"` 69 | } 70 | } 71 | f := &F{} 72 | f.f.g = 10 73 | fmt.Println(vd.Validate(f)) 74 | 75 | fmt.Println(vd.Validate(map[string]*F{"a": f})) 76 | fmt.Println(vd.Validate(map[string]map[string]*F{"a": {"b": f}})) 77 | fmt.Println(vd.Validate([]map[string]*F{{"a": f}})) 78 | fmt.Println(vd.Validate(struct { 79 | A []map[string]*F 80 | }{A: []map[string]*F{{"x": f}}})) 81 | fmt.Println(vd.Validate(map[*F]int{f: 1})) 82 | fmt.Println(vd.Validate([][1]*F{{f}})) 83 | fmt.Println(vd.Validate((*F)(nil))) 84 | fmt.Println(vd.Validate(map[string]*F{})) 85 | fmt.Println(vd.Validate(map[string]map[string]*F{})) 86 | fmt.Println(vd.Validate([]map[string]*F{})) 87 | fmt.Println(vd.Validate([]*F{})) 88 | 89 | // Output: 90 | // 91 | // email format is incorrect 92 | // true 93 | // C must be false when S.A>0 94 | // invalid d: [x y] 95 | // invalid parameter: e 96 | // {"succ":false, "error":"validation failed: f.g"} 97 | // {"succ":false, "error":"validation failed: {v for k=a}.f.g"} 98 | // {"succ":false, "error":"validation failed: {v for k=a}{v for k=b}.f.g"} 99 | // {"succ":false, "error":"validation failed: [0]{v for k=a}.f.g"} 100 | // {"succ":false, "error":"validation failed: A[0]{v for k=x}.f.g"} 101 | // {"succ":false, "error":"validation failed: {k}.f.g"} 102 | // {"succ":false, "error":"validation failed: [0][0].f.g"} 103 | // unsupport data: nil 104 | // 105 | // 106 | // 107 | // 108 | } 109 | -------------------------------------------------------------------------------- /binding/func.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | jsonpkg "encoding/json" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "time" 9 | 10 | "github.com/andeya/ameda" 11 | "google.golang.org/protobuf/proto" 12 | ) 13 | 14 | // JSONUnmarshaler is the interface implemented by types 15 | // that can unmarshal a JSON description of themselves. 16 | type JSONUnmarshaler func(data []byte, v interface{}) error 17 | 18 | // ResetJSONUnmarshaler reset the JSON Unmarshal function. 19 | // NOTE: verifyingRequired is true if the required tag is supported. 20 | // 21 | // Deprecated: please use: Default().ResetJSONUnmarshaler 22 | func ResetJSONUnmarshaler(fn JSONUnmarshaler) { 23 | defaultBinding.ResetJSONUnmarshaler(fn) 24 | } 25 | 26 | var typeUnmarshalFuncs = make(map[reflect.Type]func(string, bool) (reflect.Value, error)) 27 | 28 | func unsafeUnmarshalValue(v reflect.Value, s string, looseZeroMode bool) error { 29 | fn := typeUnmarshalFuncs[v.Type()] 30 | if fn != nil { 31 | vv, err := fn(s, looseZeroMode) 32 | if err == nil { 33 | v.Set(vv) 34 | } 35 | return err 36 | } 37 | return unmarshal(ameda.UnsafeStringToBytes(s), v.Addr().Interface()) 38 | } 39 | 40 | func unsafeUnmarshalSlice(t reflect.Type, a []string, looseZeroMode bool) (reflect.Value, error) { 41 | var err error 42 | fn := typeUnmarshalFuncs[t] 43 | if fn == nil { 44 | fn = func(s string, _ bool) (reflect.Value, error) { 45 | v := reflect.New(t) 46 | i := v.Interface() 47 | err = unmarshal(ameda.UnsafeStringToBytes(s), i) 48 | return v.Elem(), err 49 | } 50 | } 51 | v := reflect.New(reflect.SliceOf(t)).Elem() 52 | for _, s := range a { 53 | var vv reflect.Value 54 | vv, err = fn(s, looseZeroMode) 55 | if err != nil { 56 | return v, err 57 | } 58 | v = reflect.Append(v, vv) 59 | } 60 | return v, nil 61 | } 62 | 63 | func unmarshal(b []byte, i interface{}) error { 64 | switch x := i.(type) { 65 | case jsonpkg.Unmarshaler: 66 | return x.UnmarshalJSON(b) 67 | case proto.Message: 68 | return proto.Unmarshal(b, x) 69 | default: 70 | return jsonpkg.Unmarshal(b, i) 71 | } 72 | } 73 | 74 | // MustRegTypeUnmarshal registers unmarshalor function of type. 75 | // NOTE: 76 | // 77 | // panic if exist error. 78 | func MustRegTypeUnmarshal(t reflect.Type, fn func(v string, emptyAsZero bool) (reflect.Value, error)) { 79 | err := RegTypeUnmarshal(t, fn) 80 | if err != nil { 81 | panic(err) 82 | } 83 | } 84 | 85 | // RegTypeUnmarshal registers unmarshalor function of type. 86 | func RegTypeUnmarshal(t reflect.Type, fn func(v string, emptyAsZero bool) (reflect.Value, error)) error { 87 | // check 88 | switch t.Kind() { 89 | case reflect.String, reflect.Bool, 90 | reflect.Float32, reflect.Float64, 91 | reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, 92 | reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: 93 | return errors.New("registration type cannot be a basic type") 94 | case reflect.Ptr: 95 | return errors.New("registration type cannot be a pointer type") 96 | } 97 | // test 98 | vv, err := fn("", true) 99 | if err != nil { 100 | return fmt.Errorf("test fail: %s", err) 101 | } 102 | if tt := vv.Type(); tt != t { 103 | return fmt.Errorf("test fail: expect return value type is %s, but got %s", t.String(), tt.String()) 104 | } 105 | 106 | typeUnmarshalFuncs[t] = fn 107 | return nil 108 | } 109 | 110 | func init() { 111 | MustRegTypeUnmarshal(reflect.TypeOf(time.Time{}), func(v string, emptyAsZero bool) (reflect.Value, error) { 112 | if v == "" && emptyAsZero { 113 | return reflect.ValueOf(time.Time{}), nil 114 | } 115 | t, err := time.Parse(time.RFC3339, v) 116 | if err != nil { 117 | return reflect.Value{}, err 118 | } 119 | return reflect.ValueOf(t), nil 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package tagexpr 2 | 3 | import "reflect" 4 | 5 | // FieldHandler field handler 6 | type FieldHandler struct { 7 | selector string 8 | field *fieldVM 9 | expr *TagExpr 10 | } 11 | 12 | func newFieldHandler(expr *TagExpr, fieldSelector string, field *fieldVM) *FieldHandler { 13 | return &FieldHandler{ 14 | selector: fieldSelector, 15 | field: field, 16 | expr: expr, 17 | } 18 | } 19 | 20 | // StringSelector returns the field selector of string type. 21 | func (f *FieldHandler) StringSelector() string { 22 | return f.selector 23 | } 24 | 25 | // FieldSelector returns the field selector of FieldSelector type. 26 | func (f *FieldHandler) FieldSelector() FieldSelector { 27 | return FieldSelector(f.selector) 28 | } 29 | 30 | // Value returns the field value. 31 | // NOTE: 32 | // If initZero==true, initialize nil pointer to zero value 33 | func (f *FieldHandler) Value(initZero bool) reflect.Value { 34 | return f.field.reflectValueGetter(f.expr.ptr, initZero) 35 | } 36 | 37 | // EvalFuncs returns the tag expression eval functions. 38 | func (f *FieldHandler) EvalFuncs() map[ExprSelector]func() interface{} { 39 | targetTagExpr, _ := f.expr.checkout(f.selector) 40 | evals := make(map[ExprSelector]func() interface{}, len(f.field.exprs)) 41 | for k, v := range f.field.exprs { 42 | expr := v 43 | exprSelector := ExprSelector(k) 44 | evals[exprSelector] = func() interface{} { 45 | return expr.run(exprSelector.Name(), targetTagExpr) 46 | } 47 | } 48 | return evals 49 | } 50 | 51 | // StructField returns the field StructField object. 52 | func (f *FieldHandler) StructField() reflect.StructField { 53 | return f.field.structField 54 | } 55 | 56 | // ExprHandler expr handler 57 | type ExprHandler struct { 58 | base string 59 | path string 60 | selector string 61 | expr *TagExpr 62 | targetExpr *TagExpr 63 | } 64 | 65 | func newExprHandler(te, tte *TagExpr, base, es string) *ExprHandler { 66 | return &ExprHandler{ 67 | base: base, 68 | selector: es, 69 | expr: te, 70 | targetExpr: tte, 71 | } 72 | } 73 | 74 | // TagExpr returns the *TagExpr. 75 | func (e *ExprHandler) TagExpr() *TagExpr { 76 | return e.expr 77 | } 78 | 79 | // StringSelector returns the expression selector of string type. 80 | func (e *ExprHandler) StringSelector() string { 81 | return e.selector 82 | } 83 | 84 | // ExprSelector returns the expression selector of ExprSelector type. 85 | func (e *ExprHandler) ExprSelector() ExprSelector { 86 | return ExprSelector(e.selector) 87 | } 88 | 89 | // Path returns the path description of the expression. 90 | func (e *ExprHandler) Path() string { 91 | if e.path == "" { 92 | if e.targetExpr.path == "" { 93 | e.path = e.selector 94 | } else { 95 | e.path = e.targetExpr.path + FieldSeparator + e.selector 96 | } 97 | } 98 | return e.path 99 | } 100 | 101 | // Eval evaluate the value of the struct tag expression. 102 | // NOTE: 103 | // result types: float64, string, bool, nil 104 | func (e *ExprHandler) Eval() interface{} { 105 | return e.expr.s.exprs[e.selector].run(e.base, e.targetExpr) 106 | } 107 | 108 | // EvalFloat evaluates the value of the struct tag expression. 109 | // NOTE: 110 | // If the expression value type is not float64, return 0. 111 | func (e *ExprHandler) EvalFloat() float64 { 112 | r, _ := e.Eval().(float64) 113 | return r 114 | } 115 | 116 | // EvalString evaluates the value of the struct tag expression. 117 | // NOTE: 118 | // If the expression value type is not string, return "". 119 | func (e *ExprHandler) EvalString() string { 120 | r, _ := e.Eval().(string) 121 | return r 122 | } 123 | 124 | // EvalBool evaluates the value of the struct tag expression. 125 | // NOTE: 126 | // If the expression value is not 0, '' or nil, return true. 127 | func (e *ExprHandler) EvalBool() bool { 128 | return FakeBool(e.Eval()) 129 | } 130 | -------------------------------------------------------------------------------- /spec_range.go: -------------------------------------------------------------------------------- 1 | package tagexpr 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "regexp" 7 | ) 8 | 9 | type rangeCtxKey string 10 | 11 | const ( 12 | rangeKey rangeCtxKey = "#k" 13 | rangeValue rangeCtxKey = "#v" 14 | rangeLen rangeCtxKey = "##" 15 | ) 16 | 17 | type rangeKvExprNode struct { 18 | exprBackground 19 | ctxKey rangeCtxKey 20 | boolOpposite *bool 21 | signOpposite *bool 22 | } 23 | 24 | func (re *rangeKvExprNode) String() string { 25 | return string(re.ctxKey) 26 | } 27 | 28 | func (p *Expr) readRangeKvExprNode(expr *string) ExprNode { 29 | name, boolOpposite, signOpposite, found := findRangeKv(expr) 30 | if !found { 31 | return nil 32 | } 33 | operand := &rangeKvExprNode{ 34 | ctxKey: rangeCtxKey(name), 35 | boolOpposite: boolOpposite, 36 | signOpposite: signOpposite, 37 | } 38 | // fmt.Printf("operand: %#v\n", operand) 39 | return operand 40 | } 41 | 42 | var rangeKvRegexp = regexp.MustCompile(`^([\!\+\-]*)(#[kv#])([\)\[\],\+\-\*\/%><\|&!=\^ \t\\]|$)`) 43 | 44 | func findRangeKv(expr *string) (name string, boolOpposite, signOpposite *bool, found bool) { 45 | raw := *expr 46 | a := rangeKvRegexp.FindAllStringSubmatch(raw, -1) 47 | if len(a) != 1 { 48 | return 49 | } 50 | r := a[0] 51 | name = r[2] 52 | *expr = (*expr)[len(a[0][0])-len(r[3]):] 53 | prefix := r[1] 54 | if len(prefix) == 0 { 55 | found = true 56 | return 57 | } 58 | _, boolOpposite, signOpposite = getBoolAndSignOpposite(&prefix) 59 | found = true 60 | return 61 | } 62 | 63 | func (re *rangeKvExprNode) Run(ctx context.Context, _ string, _ *TagExpr) interface{} { 64 | var v interface{} 65 | switch val := ctx.Value(re.ctxKey).(type) { 66 | case reflect.Value: 67 | if !val.IsValid() || !val.CanInterface() { 68 | return nil 69 | } 70 | v = val.Interface() 71 | default: 72 | v = val 73 | } 74 | return realValue(v, re.boolOpposite, re.signOpposite) 75 | } 76 | 77 | type rangeFuncExprNode struct { 78 | exprBackground 79 | object ExprNode 80 | elemExprNode ExprNode 81 | boolOpposite *bool 82 | signOpposite *bool 83 | } 84 | 85 | func (e *rangeFuncExprNode) String() string { 86 | return "range()" 87 | } 88 | 89 | // range($, gt($v,10)) 90 | // range($, $v>10) 91 | func readRangeFuncExprNode(p *Expr, expr *string) ExprNode { 92 | boolOpposite, signOpposite, args, found := p.parseFuncSign("range", expr) 93 | if !found { 94 | return nil 95 | } 96 | if len(args) != 2 { 97 | return nil 98 | } 99 | return &rangeFuncExprNode{ 100 | boolOpposite: boolOpposite, 101 | signOpposite: signOpposite, 102 | object: args[0], 103 | elemExprNode: args[1], 104 | } 105 | } 106 | 107 | func (e *rangeFuncExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 108 | var r []interface{} 109 | obj := e.object.Run(ctx, currField, tagExpr) 110 | // fmt.Printf("%v\n", obj) 111 | objval := reflect.ValueOf(obj) 112 | switch objval.Kind() { 113 | case reflect.Array, reflect.Slice: 114 | count := objval.Len() 115 | r = make([]interface{}, count) 116 | ctx = context.WithValue(ctx, rangeLen, count) 117 | for i := 0; i < count; i++ { 118 | // fmt.Printf("%#v, (%v)\n", e.elemExprNode, objval.Index(i)) 119 | r[i] = realValue(e.elemExprNode.Run( 120 | context.WithValue( 121 | context.WithValue( 122 | ctx, 123 | rangeKey, i, 124 | ), 125 | rangeValue, objval.Index(i), 126 | ), 127 | currField, tagExpr, 128 | ), e.boolOpposite, e.signOpposite) 129 | } 130 | case reflect.Map: 131 | keys := objval.MapKeys() 132 | count := len(keys) 133 | r = make([]interface{}, count) 134 | ctx = context.WithValue(ctx, rangeLen, count) 135 | for i, key := range keys { 136 | r[i] = realValue(e.elemExprNode.Run( 137 | context.WithValue( 138 | context.WithValue( 139 | ctx, 140 | rangeKey, key, 141 | ), 142 | rangeValue, objval.MapIndex(key), 143 | ), 144 | currField, tagExpr, 145 | ), e.boolOpposite, e.signOpposite) 146 | } 147 | default: 148 | } 149 | return r 150 | } 151 | -------------------------------------------------------------------------------- /binding/example_test.go: -------------------------------------------------------------------------------- 1 | package binding_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/andeya/goutil/httpbody" 14 | 15 | "github.com/bytedance/go-tagexpr/v2/binding" 16 | ) 17 | 18 | func Example() { 19 | type InfoRequest struct { 20 | Name string `path:"name"` 21 | Year []int `query:"year"` 22 | Pages []uint64 `query:"pages"` 23 | Email *string `json:"email" vd:"email($)"` 24 | Friendly bool `json:"friendly"` 25 | Pie float32 `json:"pie,required"` 26 | Hobby []string `json:",required"` 27 | BodyNotFound *int `json:"BodyNotFound"` 28 | Authorization string `header:"Authorization,required" vd:"$=='Basic 123456'"` 29 | userIdHeader string `header:"x-user_ID,required"` 30 | SessionID string `cookie:"sessionid,required"` 31 | AutoBody string 32 | AutoNotFound *string 33 | TimeRFC3339 time.Time `query:"t"` 34 | } 35 | 36 | binding.MustRegTypeUnmarshal(reflect.TypeOf([]uint64{}), func(v string, emptyAsZero bool) (reflect.Value, error) { 37 | if v == "" && emptyAsZero { 38 | return reflect.ValueOf([]uint64{}), nil 39 | } 40 | 41 | ss := strings.Split(v, ",") 42 | t := make([]uint64, 0, len(ss)) 43 | 44 | for _, s := range ss { 45 | i, err := strconv.ParseUint(s, 10, 64) 46 | if err != nil { 47 | return reflect.ValueOf([]uint64{}), err 48 | } 49 | t = append(t, i) 50 | } 51 | 52 | return reflect.ValueOf(t), nil 53 | }) 54 | 55 | args := new(InfoRequest) 56 | binder := binding.New(nil) 57 | err := binder.BindAndValidate(args, requestExample(), new(testPathParams)) 58 | 59 | fmt.Println("bind and validate result:") 60 | 61 | fmt.Printf("error: %v\n", err) 62 | 63 | b, _ := json.MarshalIndent(args, "", " ") 64 | fmt.Printf("args JSON string:\n%s\n", b) 65 | 66 | // Output: 67 | // request: 68 | // POST /info/henrylee2cn?year=2018&year=2019&t=2019-09-04T18%3A04%3A08%2B08%3A00&pages=1,2,3 HTTP/1.1 69 | // Host: localhost 70 | // User-Agent: Go-http-client/1.1 71 | // Transfer-Encoding: chunked 72 | // Authorization: Basic 123456 73 | // Content-Type: application/json;charset=utf-8 74 | // Cookie: sessionid=987654 75 | // X-User_id: 123456 76 | // 77 | // 83 78 | // {"AutoBody":"autobody_test","Hobby":["Coding","Mountain climbing"],"email":"henrylee2cn@gmail.com","friendly":true,"pie":3.1415926} 79 | // 0 80 | // 81 | // bind and validate result: 82 | // error: 83 | // args JSON string: 84 | // { 85 | // "Name": "henrylee2cn", 86 | // "Year": [ 87 | // 2018, 88 | // 2019 89 | // ], 90 | // "Pages": [ 91 | // 1, 92 | // 2, 93 | // 3 94 | // ], 95 | // "email": "henrylee2cn@gmail.com", 96 | // "friendly": true, 97 | // "pie": 3.1415925, 98 | // "Hobby": [ 99 | // "Coding", 100 | // "Mountain climbing" 101 | // ], 102 | // "BodyNotFound": null, 103 | // "Authorization": "Basic 123456", 104 | // "SessionID": "987654", 105 | // "AutoBody": "autobody_test", 106 | // "AutoNotFound": null, 107 | // "TimeRFC3339": "2019-09-04T18:04:08+08:00" 108 | // } 109 | } 110 | 111 | func requestExample() *http.Request { 112 | contentType, bodyReader, _ := httpbody.NewJSONBody(map[string]interface{}{ 113 | "email": "henrylee2cn@gmail.com", 114 | "friendly": true, 115 | "pie": 3.1415926, 116 | "Hobby": []string{"Coding", "Mountain climbing"}, 117 | "AutoBody": "autobody_test", 118 | }) 119 | header := make(http.Header) 120 | header.Add("Content-Type", contentType) 121 | header.Add("Authorization", "Basic 123456") 122 | header.Add("x-user_ID", "123456") 123 | cookies := []*http.Cookie{ 124 | {Name: "sessionid", Value: "987654"}, 125 | } 126 | req := newRequest("http://localhost/info/henrylee2cn?year=2018&year=2019&t=2019-09-04T18%3A04%3A08%2B08%3A00&pages=1,2,3", header, cookies, bodyReader) 127 | req.Method = "POST" 128 | var w bytes.Buffer 129 | req.Write(&w) 130 | fmt.Printf("request:\n%s", strings.Replace(w.String(), "\r\n", "\n", -1)) 131 | 132 | bodyReader.(*bytes.Reader).Seek(0, 0) 133 | return req 134 | } 135 | -------------------------------------------------------------------------------- /binding/gjson/internal/caching/pcache.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ByteDance Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package caching 18 | 19 | import ( 20 | "sync" 21 | "sync/atomic" 22 | "unsafe" 23 | 24 | "github.com/bytedance/go-tagexpr/v2/binding/gjson/internal/rt" 25 | ) 26 | 27 | /** Program Map **/ 28 | 29 | const ( 30 | _LoadFactor = 0.5 31 | _InitCapacity = 4096 // must be a power of 2 32 | ) 33 | 34 | type _ProgramMap struct { 35 | n uint64 36 | m uint32 37 | b []_ProgramEntry 38 | } 39 | 40 | type _ProgramEntry struct { 41 | vt *rt.GoType 42 | fn interface{} 43 | } 44 | 45 | func newProgramMap() *_ProgramMap { 46 | return &_ProgramMap{ 47 | n: 0, 48 | m: _InitCapacity - 1, 49 | b: make([]_ProgramEntry, _InitCapacity), 50 | } 51 | } 52 | 53 | func (mips *_ProgramMap) copy() *_ProgramMap { 54 | fork := &_ProgramMap{ 55 | n: mips.n, 56 | m: mips.m, 57 | b: make([]_ProgramEntry, len(mips.b)), 58 | } 59 | for i, f := range mips.b { 60 | fork.b[i] = f 61 | } 62 | return fork 63 | } 64 | 65 | func (mips *_ProgramMap) get(vt *rt.GoType) interface{} { 66 | i := mips.m + 1 67 | p := vt.Hash & mips.m 68 | 69 | /* linear probing */ 70 | for ; i > 0; i-- { 71 | if b := mips.b[p]; b.vt == vt { 72 | return b.fn 73 | } else { 74 | p = (p + 1) & mips.m 75 | } 76 | } 77 | 78 | /* not found */ 79 | return nil 80 | } 81 | 82 | func (mips *_ProgramMap) add(vt *rt.GoType, fn interface{}) *_ProgramMap { 83 | p := mips.copy() 84 | f := float64(atomic.LoadUint64(&p.n)+1) / float64(p.m+1) 85 | 86 | /* check for load factor */ 87 | if f > _LoadFactor { 88 | p = p.rehash() 89 | } 90 | 91 | /* insert the value */ 92 | p.insert(vt, fn) 93 | return p 94 | } 95 | 96 | func (mips *_ProgramMap) rehash() *_ProgramMap { 97 | c := (mips.m + 1) << 1 98 | r := &_ProgramMap{m: c - 1, b: make([]_ProgramEntry, int(c))} 99 | 100 | /* rehash every entry */ 101 | for i := uint32(0); i <= mips.m; i++ { 102 | if b := mips.b[i]; b.vt != nil { 103 | r.insert(b.vt, b.fn) 104 | } 105 | } 106 | 107 | /* rebuild successful */ 108 | return r 109 | } 110 | 111 | func (mips *_ProgramMap) insert(vt *rt.GoType, fn interface{}) { 112 | h := vt.Hash 113 | p := h & mips.m 114 | 115 | /* linear probing */ 116 | for i := uint32(0); i <= mips.m; i++ { 117 | if b := &mips.b[p]; b.vt != nil { 118 | p += 1 119 | p &= mips.m 120 | } else { 121 | b.vt = vt 122 | b.fn = fn 123 | atomic.AddUint64(&mips.n, 1) 124 | return 125 | } 126 | } 127 | 128 | /* should never happen */ 129 | panic("no available slots") 130 | } 131 | 132 | /** RCU Program Cache **/ 133 | 134 | type ProgramCache struct { 135 | m sync.Mutex 136 | p unsafe.Pointer 137 | } 138 | 139 | func CreateProgramCache() *ProgramCache { 140 | return &ProgramCache{ 141 | m: sync.Mutex{}, 142 | p: unsafe.Pointer(newProgramMap()), 143 | } 144 | } 145 | 146 | func (c *ProgramCache) Get(vt *rt.GoType) interface{} { 147 | return (*_ProgramMap)(atomic.LoadPointer(&c.p)).get(vt) 148 | } 149 | 150 | func (c *ProgramCache) Compute(vt *rt.GoType, compute func(*rt.GoType) (interface{}, error)) (interface{}, error) { 151 | var err error 152 | var val interface{} 153 | 154 | /* use defer to prevent inlining of this function */ 155 | c.m.Lock() 156 | defer c.m.Unlock() 157 | 158 | /* double check with write lock held */ 159 | if val = c.Get(vt); val != nil { 160 | return val, nil 161 | } 162 | 163 | /* compute the value */ 164 | if val, err = compute(vt); err != nil { 165 | return nil, err 166 | } 167 | 168 | /* update the RCU cache */ 169 | atomic.StorePointer(&c.p, unsafe.Pointer((*_ProgramMap)(atomic.LoadPointer(&c.p)).add(vt, val))) 170 | return val, nil 171 | } 172 | -------------------------------------------------------------------------------- /tagparser.go: -------------------------------------------------------------------------------- 1 | package tagexpr 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | type namedTagExpr struct { 10 | exprSelector string 11 | expr *Expr 12 | } 13 | 14 | const ( 15 | tagOmit = "-" 16 | tagOmitNil = "?" 17 | ) 18 | 19 | func (f *fieldVM) parseExprs(tag string) error { 20 | switch tag { 21 | case tagOmit, tagOmitNil: 22 | f.tagOp = tag 23 | return nil 24 | } 25 | 26 | kvs, err := parseTag(tag) 27 | if err != nil { 28 | return err 29 | } 30 | exprSelectorPrefix := f.structField.Name 31 | 32 | for exprSelector, exprString := range kvs { 33 | expr, err := parseExpr(exprString) 34 | if err != nil { 35 | return err 36 | } 37 | if exprSelector == ExprNameSeparator { 38 | exprSelector = exprSelectorPrefix 39 | } else { 40 | exprSelector = exprSelectorPrefix + ExprNameSeparator + exprSelector 41 | } 42 | f.exprs[exprSelector] = expr 43 | f.origin.exprs[exprSelector] = expr 44 | f.origin.exprSelectorList = append(f.origin.exprSelectorList, exprSelector) 45 | } 46 | return nil 47 | } 48 | 49 | func parseTag(tag string) (map[string]string, error) { 50 | s := tag 51 | ptr := &s 52 | kvs := make(map[string]string) 53 | for { 54 | one, err := readOneExpr(ptr) 55 | if err != nil { 56 | return nil, err 57 | } 58 | if one == "" { 59 | return kvs, nil 60 | } 61 | key, val := splitExpr(one) 62 | if val == "" { 63 | return nil, fmt.Errorf("syntax error: %q expression string can not be empty", tag) 64 | } 65 | if _, ok := kvs[key]; ok { 66 | return nil, fmt.Errorf("syntax error: %q duplicate expression name %q", tag, key) 67 | } 68 | kvs[key] = val 69 | } 70 | } 71 | 72 | func splitExpr(one string) (key, val string) { 73 | one = strings.TrimSpace(one) 74 | if one == "" { 75 | return DefaultExprName, "" 76 | } 77 | var rs []rune 78 | for _, r := range one { 79 | if r == '@' || 80 | r == '_' || 81 | (r >= '0' && r <= '9') || 82 | (r >= 'A' && r <= 'Z') || 83 | (r >= 'a' && r <= 'z') { 84 | rs = append(rs, r) 85 | } else { 86 | break 87 | } 88 | } 89 | key = string(rs) 90 | val = strings.TrimSpace(one[len(key):]) 91 | if val == "" || val[0] != ':' { 92 | return DefaultExprName, one 93 | } 94 | val = val[1:] 95 | if key == "" { 96 | key = DefaultExprName 97 | } 98 | return key, val 99 | } 100 | 101 | func readOneExpr(tag *string) (string, error) { 102 | var s = *(trimRightSpace(trimLeftSpace(tag))) 103 | s = strings.TrimLeft(s, ";") 104 | if s == "" { 105 | return "", nil 106 | } 107 | if s[len(s)-1] != ';' { 108 | s += ";" 109 | } 110 | a := strings.SplitAfter(strings.Replace(s, "\\'", "##", -1), ";") 111 | var idx = -1 112 | var patch int 113 | for _, v := range a { 114 | idx += len(v) 115 | count := strings.Count(v, "'") 116 | if (count+patch)%2 == 0 { 117 | *tag = s[idx+1:] 118 | return s[:idx], nil 119 | } 120 | if count > 0 { 121 | patch++ 122 | } 123 | } 124 | return "", fmt.Errorf("syntax error: %q unclosed single quote \"'\"", s) 125 | } 126 | 127 | func trimLeftSpace(p *string) *string { 128 | *p = strings.TrimLeftFunc(*p, unicode.IsSpace) 129 | return p 130 | } 131 | 132 | func trimRightSpace(p *string) *string { 133 | *p = strings.TrimRightFunc(*p, unicode.IsSpace) 134 | return p 135 | } 136 | 137 | func readPairedSymbol(p *string, left, right rune) *string { 138 | s := *p 139 | if len(s) == 0 || rune(s[0]) != left { 140 | return nil 141 | } 142 | s = s[1:] 143 | var last1 = left 144 | var last2 rune 145 | var leftLevel, rightLevel int 146 | var escapeIndexes = make(map[int]bool) 147 | var realEqual, escapeEqual bool 148 | for i, r := range s { 149 | if realEqual, escapeEqual = equalRune(right, r, last1, last2); realEqual { 150 | if leftLevel == rightLevel { 151 | *p = s[i+1:] 152 | var sub = make([]rune, 0, i) 153 | for k, v := range s[:i] { 154 | if !escapeIndexes[k] { 155 | sub = append(sub, v) 156 | } 157 | } 158 | s = string(sub) 159 | return &s 160 | } 161 | rightLevel++ 162 | } else if escapeEqual { 163 | escapeIndexes[i-1] = true 164 | } else if realEqual, escapeEqual = equalRune(left, r, last1, last2); realEqual { 165 | leftLevel++ 166 | } else if escapeEqual { 167 | escapeIndexes[i-1] = true 168 | } 169 | last2 = last1 170 | last1 = r 171 | } 172 | return nil 173 | } 174 | 175 | func equalRune(a, b, last1, last2 rune) (real, escape bool) { 176 | if a == b { 177 | real = last1 != '\\' || last2 == '\\' 178 | escape = last1 == '\\' && last2 != '\\' 179 | } 180 | return 181 | } 182 | -------------------------------------------------------------------------------- /binding/gjson/internal/rt/fastvalue.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ByteDance Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package rt 18 | 19 | import ( 20 | "reflect" 21 | "unsafe" 22 | ) 23 | 24 | var ( 25 | reflectRtypeItab = findReflectRtypeItab() 26 | ) 27 | 28 | const ( 29 | F_direct = 1 << 5 30 | F_kind_mask = (1 << 5) - 1 31 | ) 32 | 33 | type GoType struct { 34 | Size uintptr 35 | PtrData uintptr 36 | Hash uint32 37 | Flags uint8 38 | Align uint8 39 | FieldAlign uint8 40 | KindFlags uint8 41 | Traits unsafe.Pointer 42 | GCData *byte 43 | Str int32 44 | PtrToSelf int32 45 | } 46 | 47 | func (self *GoType) Kind() reflect.Kind { 48 | return reflect.Kind(self.KindFlags & F_kind_mask) 49 | } 50 | 51 | func (self *GoType) Pack() (t reflect.Type) { 52 | (*GoIface)(unsafe.Pointer(&t)).Itab = reflectRtypeItab 53 | (*GoIface)(unsafe.Pointer(&t)).Value = unsafe.Pointer(self) 54 | return 55 | } 56 | 57 | func (self *GoType) String() string { 58 | return self.Pack().String() 59 | } 60 | 61 | type GoMap struct { 62 | Count int 63 | Flags uint8 64 | B uint8 65 | Overflow uint16 66 | Hash0 uint32 67 | Buckets unsafe.Pointer 68 | OldBuckets unsafe.Pointer 69 | Evacuate uintptr 70 | Extra unsafe.Pointer 71 | } 72 | 73 | type GoMapIterator struct { 74 | K unsafe.Pointer 75 | V unsafe.Pointer 76 | T *GoMapType 77 | H *GoMap 78 | Buckets unsafe.Pointer 79 | Bptr *unsafe.Pointer 80 | Overflow *[]unsafe.Pointer 81 | OldOverflow *[]unsafe.Pointer 82 | StartBucket uintptr 83 | Offset uint8 84 | Wrapped bool 85 | B uint8 86 | I uint8 87 | Bucket uintptr 88 | CheckBucket uintptr 89 | } 90 | 91 | type GoItab struct { 92 | it unsafe.Pointer 93 | Vt *GoType 94 | hv uint32 95 | _ [4]byte 96 | fn [1]uintptr 97 | } 98 | 99 | type GoIface struct { 100 | Itab *GoItab 101 | Value unsafe.Pointer 102 | } 103 | 104 | type GoEface struct { 105 | Type *GoType 106 | Value unsafe.Pointer 107 | } 108 | 109 | func (self GoEface) Pack() (v interface{}) { 110 | *(*GoEface)(unsafe.Pointer(&v)) = self 111 | return 112 | } 113 | 114 | type GoPtrType struct { 115 | GoType 116 | Elem *GoType 117 | } 118 | 119 | type GoMapType struct { 120 | GoType 121 | Key *GoType 122 | Elem *GoType 123 | Bucket *GoType 124 | Hasher func(unsafe.Pointer, uintptr) uintptr 125 | KeySize uint8 126 | ElemSize uint8 127 | BucketSize uint16 128 | Flags uint32 129 | } 130 | 131 | func (self *GoMapType) IndirectElem() bool { 132 | return self.Flags&2 != 0 133 | } 134 | 135 | type GoStructType struct { 136 | GoType 137 | Pkg *byte 138 | Fields []GoStructField 139 | } 140 | 141 | type GoStructField struct { 142 | Name *byte 143 | Type *GoType 144 | OffEmbed uintptr 145 | } 146 | 147 | type GoInterfaceType struct { 148 | GoType 149 | PkgPath *byte 150 | Methods []GoInterfaceMethod 151 | } 152 | 153 | type GoInterfaceMethod struct { 154 | Name int32 155 | Type int32 156 | } 157 | 158 | type GoSlice struct { 159 | Ptr unsafe.Pointer 160 | Len int 161 | Cap int 162 | } 163 | 164 | type GoString struct { 165 | Ptr unsafe.Pointer 166 | Len int 167 | } 168 | 169 | func PtrElem(t *GoType) *GoType { 170 | return (*GoPtrType)(unsafe.Pointer(t)).Elem 171 | } 172 | 173 | func MapType(t *GoType) *GoMapType { 174 | return (*GoMapType)(unsafe.Pointer(t)) 175 | } 176 | 177 | func IfaceType(t *GoType) *GoInterfaceType { 178 | return (*GoInterfaceType)(unsafe.Pointer(t)) 179 | } 180 | 181 | func UnpackType(t reflect.Type) *GoType { 182 | return (*GoType)((*GoIface)(unsafe.Pointer(&t)).Value) 183 | } 184 | 185 | func UnpackEface(v interface{}) GoEface { 186 | return *(*GoEface)(unsafe.Pointer(&v)) 187 | } 188 | 189 | func UnpackIface(v interface{}) GoIface { 190 | return *(*GoIface)(unsafe.Pointer(&v)) 191 | } 192 | 193 | func findReflectRtypeItab() *GoItab { 194 | v := reflect.TypeOf(struct{}{}) 195 | return (*GoIface)(unsafe.Pointer(&v)).Itab 196 | } 197 | -------------------------------------------------------------------------------- /validator/validator.go: -------------------------------------------------------------------------------- 1 | // Package validator is a powerful validator that supports struct tag expression. 2 | // 3 | // Copyright 2019 Bytedance Inc. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | package validator 17 | 18 | import ( 19 | "errors" 20 | "io" 21 | "reflect" 22 | "strings" 23 | _ "unsafe" 24 | 25 | tagexpr "github.com/bytedance/go-tagexpr/v2" 26 | ) 27 | 28 | const ( 29 | // MatchExprName the name of the expression used for validation 30 | MatchExprName = tagexpr.DefaultExprName 31 | // ErrMsgExprName the name of the expression used to specify the message 32 | // returned when validation failed 33 | ErrMsgExprName = "msg" 34 | ) 35 | 36 | // Validator struct fields validator 37 | type Validator struct { 38 | vm *tagexpr.VM 39 | errFactory func(failPath, msg string) error 40 | } 41 | 42 | // New creates a struct fields validator. 43 | func New(tagName string) *Validator { 44 | v := &Validator{ 45 | vm: tagexpr.New(tagName), 46 | errFactory: defaultErrorFactory, 47 | } 48 | return v 49 | } 50 | 51 | // VM returns the struct tag expression interpreter. 52 | func (v *Validator) VM() *tagexpr.VM { 53 | return v.vm 54 | } 55 | 56 | // Validate validates whether the fields of value is valid. 57 | // NOTE: 58 | // If checkAll=true, validate all the error. 59 | func (v *Validator) Validate(value interface{}, checkAll ...bool) error { 60 | var all bool 61 | if len(checkAll) > 0 { 62 | all = checkAll[0] 63 | } 64 | var errs = make([]error, 0, 8) 65 | err := v.vm.RunAny(value, func(te *tagexpr.TagExpr, err error) error { 66 | if err != nil { 67 | errs = append(errs, err) 68 | if all { 69 | return nil 70 | } 71 | return io.EOF 72 | } 73 | nilParentFields := make(map[string]bool, 16) 74 | err = te.Range(func(eh *tagexpr.ExprHandler) error { 75 | if strings.Contains(eh.StringSelector(), tagexpr.ExprNameSeparator) { 76 | return nil 77 | } 78 | r := eh.Eval() 79 | if r == nil { 80 | return nil 81 | } 82 | rerr, ok := r.(error) 83 | if !ok && tagexpr.FakeBool(r) { 84 | return nil 85 | } 86 | // Ignore this error if the value of the parent is nil 87 | if pfs, ok := eh.ExprSelector().ParentField(); ok { 88 | if nilParentFields[pfs] { 89 | return nil 90 | } 91 | if fh, ok := eh.TagExpr().Field(pfs); ok { 92 | v := fh.Value(false) 93 | if !v.IsValid() || (v.Kind() == reflect.Ptr && v.IsNil()) { 94 | nilParentFields[pfs] = true 95 | return nil 96 | } 97 | } 98 | } 99 | msg := eh.TagExpr().EvalString(eh.StringSelector() + tagexpr.ExprNameSeparator + ErrMsgExprName) 100 | if msg == "" && rerr != nil { 101 | msg = rerr.Error() 102 | } 103 | errs = append(errs, v.errFactory(eh.Path(), msg)) 104 | if all { 105 | return nil 106 | } 107 | return io.EOF 108 | }) 109 | if err != nil && !all { 110 | return err 111 | } 112 | return nil 113 | }) 114 | if err != io.EOF && err != nil { 115 | return err 116 | } 117 | switch len(errs) { 118 | case 0: 119 | return nil 120 | case 1: 121 | return errs[0] 122 | default: 123 | var errStr string 124 | for _, e := range errs { 125 | errStr += e.Error() + "\t" 126 | } 127 | return errors.New(errStr[:len(errStr)-1]) 128 | } 129 | } 130 | 131 | // SetErrorFactory customizes the factory of validation error. 132 | // NOTE: 133 | // If errFactory==nil, the default is used 134 | func (v *Validator) SetErrorFactory(errFactory func(failPath, msg string) error) *Validator { 135 | if errFactory == nil { 136 | errFactory = defaultErrorFactory 137 | } 138 | v.errFactory = errFactory 139 | return v 140 | } 141 | 142 | // Error validate error 143 | type Error struct { 144 | FailPath, Msg string 145 | } 146 | 147 | // Error implements error interface. 148 | func (e *Error) Error() string { 149 | if e.Msg != "" { 150 | return e.Msg 151 | } 152 | return "invalid parameter: " + e.FailPath 153 | } 154 | 155 | //go:nosplit 156 | func defaultErrorFactory(failPath, msg string) error { 157 | return &Error{ 158 | FailPath: failPath, 159 | Msg: msg, 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /binding/README.md: -------------------------------------------------------------------------------- 1 | # binding [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/bytedance/go-tagexpr/v2/binding) 2 | 3 | A powerful HTTP request parameters binder that supports struct tag expression. 4 | 5 | ## Example 6 | 7 | ```go 8 | func Example() { 9 | type InfoRequest struct { 10 | Name string `path:"name"` 11 | Year []int `query:"year"` 12 | Email *string `json:"email" vd:"email($)"` 13 | Friendly bool `json:"friendly"` 14 | Status string `json:"status" default:"single"` 15 | Pie float32 `json:"pie,required"` 16 | Hobby []string `json:",required"` 17 | BodyNotFound *int `json:"BodyNotFound"` 18 | Authorization string `header:"Authorization,required" vd:"$=='Basic 123456'"` 19 | SessionID string `cookie:"sessionid,required"` 20 | AutoBody string 21 | AutoNotFound *string 22 | TimeRFC3339 time.Time `query:"t"` 23 | } 24 | 25 | args := new(InfoRequest) 26 | binder := binding.New(nil) 27 | err := binder.BindAndValidate(args, requestExample(), new(testPathParams)) 28 | 29 | fmt.Println("bind and validate result:") 30 | 31 | fmt.Printf("error: %v\n", err) 32 | 33 | b, _ := json.MarshalIndent(args, "", " ") 34 | fmt.Printf("args JSON string:\n%s\n", b) 35 | 36 | // Output: 37 | // request: 38 | // POST /info/henrylee2cn?year=2018&year=2019&t=2019-09-04T18%3A04%3A08%2B08%3A00 HTTP/1.1 39 | // Host: localhost 40 | // User-Agent: Go-http-client/1.1 41 | // Transfer-Encoding: chunked 42 | // Authorization: Basic 123456 43 | // Content-Type: application/json;charset=utf-8 44 | // Cookie: sessionid=987654 45 | // 46 | // 83 47 | // {"AutoBody":"autobody_test","Hobby":["Coding","Mountain climbing"],"email":"henrylee2cn@gmail.com","friendly":true,"pie":3.1415926} 48 | // 0 49 | // 50 | // bind and validate result: 51 | // error: 52 | // args JSON string: 53 | // { 54 | // "Name": "henrylee2cn", 55 | // "Year": [ 56 | // 2018, 57 | // 2019 58 | // ], 59 | // "email": "henrylee2cn@gmail.com", 60 | // "friendly": true, 61 | // "status": "single", 62 | // "pie": 3.1415925, 63 | // "Hobby": [ 64 | // "Coding", 65 | // "Mountain climbing" 66 | // ], 67 | // "BodyNotFound": null, 68 | // "Authorization": "Basic 123456", 69 | // "SessionID": "987654", 70 | // "AutoBody": "autobody_test", 71 | // "AutoNotFound": null, 72 | // "TimeRFC3339": "2019-09-04T18:04:08+08:00" 73 | // } 74 | } 75 | ... 76 | ``` 77 | 78 | ## Syntax 79 | 80 | The parameter position in HTTP request: 81 | 82 | |expression|renameable|description| 83 | |----------|----------|-----------| 84 | |`path:"$name"` or `path:"$name,required"`|Yes|URL path parameter| 85 | |`query:"$name"` or `query:"$name,required"`|Yes|URL query parameter| 86 | |`raw_body:""` or `raw_body:"required"`|Yes|The raw bytes of body| 87 | |`form:"$name"` or `form:"$name,required"`|Yes|The field in body, support:
`application/x-www-form-urlencoded`,
`multipart/form-data`| 88 | |`protobuf:"...(raw syntax)"`|No|The field in body, support:
`application/x-protobuf`| 89 | |`json:"$name"` or `json:"$name,required"`|No|The field in body, support:
`application/json`| 90 | |`header:"$name"` or `header:"$name,required"`|Yes|Header parameter| 91 | |`cookie:"$name"` or `cookie:"$name,required"`|Yes|Cookie parameter| 92 | |`default:"$value"`|Yes|Default parameter| 93 | |`vd:"...(tagexpr validator syntax)"`|Yes|The tagexpr expression of validator| 94 | 95 | **NOTE:** 96 | 97 | - `"$name"` is variable placeholder 98 | - If `"$name"` is empty, use the name of field 99 | - If `"$name"` is `-`, omit the field 100 | - Expression `required` or `req` indicates that the parameter is required 101 | - `default:"$value"` defines the default value for fallback when no binding is successful 102 | - If no position is tagged, try bind parameters from the body when the request has body, 103 |
otherwise try bind from the URL query 104 | - When there is unexportable and no tags, omit the field 105 | - When there are multiple tags, or exportable and no tags, the order in which to try to bind is: 106 | 1. path 107 | 2. form 108 | 3. query 109 | 4. cookie 110 | 5. header 111 | 6. protobuf 112 | 7. json 113 | 8. default 114 | 115 | ## Type Unmarshalor 116 | 117 | TimeRFC3339-binding function is registered by default. 118 | 119 | Register your own binding function for the specified type, e.g.: 120 | 121 | ```go 122 | MustRegTypeUnmarshal(reflect.TypeOf(time.Time{}), func(v string, emptyAsZero bool) (reflect.Value, error) { 123 | if v == "" && emptyAsZero { 124 | return reflect.ValueOf(time.Time{}), nil 125 | } 126 | t, err := time.Parse(time.RFC3339, v) 127 | if err != nil { 128 | return reflect.Value{}, err 129 | } 130 | return reflect.ValueOf(t), nil 131 | }) 132 | ``` 133 | -------------------------------------------------------------------------------- /binding/tag_names.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "net/textproto" 5 | "reflect" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/andeya/goutil" 10 | ) 11 | 12 | const ( 13 | tagRequired = "required" 14 | tagRequired2 = "req" 15 | defaultTagPath = "path" 16 | defaultTagQuery = "query" 17 | defaultTagHeader = "header" 18 | defaultTagCookie = "cookie" 19 | defaultTagRawbody = "raw_body" 20 | defaultTagForm = "form" 21 | defaultTagValidator = "vd" 22 | tagProtobuf = "protobuf" 23 | tagJSON = "json" 24 | tagDefault = "default" 25 | ) 26 | 27 | // Config the struct tag naming and so on 28 | type Config struct { 29 | // LooseZeroMode if set to true, 30 | // the empty string request parameter is bound to the zero value of parameter. 31 | // NOTE: Suitable for these parameter types: query/header/cookie/form . 32 | LooseZeroMode bool 33 | // PathParam use 'path' by default when empty 34 | PathParam string 35 | // Query use 'query' by default when empty 36 | Query string 37 | // Header use 'header' by default when empty 38 | Header string 39 | // Cookie use 'cookie' by default when empty 40 | Cookie string 41 | // RawBody use 'raw' by default when empty 42 | RawBody string 43 | // FormBody use 'form' by default when empty 44 | FormBody string 45 | // Validator use 'vd' by default when empty 46 | Validator string 47 | // protobufBody use 'protobuf' by default when empty 48 | protobufBody string 49 | // jsonBody use 'json' by default when empty 50 | jsonBody string 51 | // defaultVal use 'default' by default when empty 52 | defaultVal string 53 | 54 | list []string 55 | } 56 | 57 | func (t *Config) init() { 58 | t.list = []string{ 59 | goutil.InitAndGetString(&t.PathParam, defaultTagPath), 60 | goutil.InitAndGetString(&t.Query, defaultTagQuery), 61 | goutil.InitAndGetString(&t.Header, defaultTagHeader), 62 | goutil.InitAndGetString(&t.Cookie, defaultTagCookie), 63 | goutil.InitAndGetString(&t.RawBody, defaultTagRawbody), 64 | goutil.InitAndGetString(&t.FormBody, defaultTagForm), 65 | goutil.InitAndGetString(&t.Validator, defaultTagValidator), 66 | goutil.InitAndGetString(&t.protobufBody, tagProtobuf), 67 | goutil.InitAndGetString(&t.jsonBody, tagJSON), 68 | goutil.InitAndGetString(&t.defaultVal, tagDefault), 69 | } 70 | } 71 | 72 | func (t *Config) parse(field reflect.StructField) tagKVs { 73 | tag := field.Tag 74 | fieldName := field.Name 75 | 76 | kvs := make(tagKVs, 0, len(t.list)) 77 | s := string(tag) 78 | 79 | for _, name := range t.list { 80 | value, ok := tag.Lookup(name) 81 | if !ok { 82 | continue 83 | } 84 | if name != t.defaultVal && value != "-" { 85 | value = strings.Replace(strings.TrimSpace(value), " ", "", -1) 86 | value = strings.Replace(value, "\t", "", -1) 87 | if name == t.RawBody { 88 | info := newTagInfo(value, false) 89 | if info.required || info.paramName == tagRequired { 90 | value = "," + tagRequired 91 | } 92 | } else if value == "" { 93 | value = fieldName 94 | } else if value == ","+tagRequired { 95 | value = fieldName + value 96 | } 97 | } 98 | kvs = append(kvs, &tagKV{name: name, value: value, pos: strings.Index(s, name)}) 99 | } 100 | sort.Sort(kvs) 101 | return kvs 102 | } 103 | 104 | type tagKV struct { 105 | name string 106 | value string 107 | pos int 108 | } 109 | 110 | type tagInfo struct { 111 | paramIn in 112 | paramName string 113 | required bool 114 | namePath string 115 | 116 | requiredError, typeError, cannotError, contentTypeError error 117 | } 118 | 119 | func (t *tagKV) toInfo(isHeader bool) *tagInfo { 120 | return newTagInfo(t.value, isHeader) 121 | } 122 | 123 | func newTagInfo(value string, isHeader bool) *tagInfo { 124 | info := new(tagInfo) 125 | for i, v := range strings.Split(value, ",") { 126 | v = strings.TrimSpace(v) 127 | if i == 0 { 128 | info.paramName = v 129 | } else { 130 | if v == tagRequired || v == tagRequired2 { 131 | info.required = true 132 | } 133 | } 134 | } 135 | if isHeader { 136 | info.paramName = textproto.CanonicalMIMEHeaderKey(info.paramName) 137 | } 138 | return info 139 | } 140 | 141 | type tagKVs []*tagKV 142 | 143 | // Len is the number of elements in the collection. 144 | func (a tagKVs) Len() int { 145 | return len(a) 146 | } 147 | 148 | // Less reports whether the element with 149 | // index i should sort before the element with index j. 150 | func (a tagKVs) Less(i, j int) bool { 151 | return a[i].pos < a[j].pos 152 | } 153 | 154 | // Swap swaps the elements with indexes i and j. 155 | func (a tagKVs) Swap(i, j int) { 156 | a[i], a[j] = a[j], a[i] 157 | } 158 | 159 | func (a tagKVs) lookup(name string) (string, bool) { 160 | for _, v := range a { 161 | if v.name == name { 162 | return v.value, true 163 | } 164 | } 165 | return "", false 166 | } 167 | -------------------------------------------------------------------------------- /binding/receiver.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "mime/multipart" 5 | "net/http" 6 | "net/url" 7 | "reflect" 8 | 9 | "github.com/bytedance/go-tagexpr/v2" 10 | ) 11 | 12 | type in uint8 13 | 14 | const ( 15 | undefined in = iota 16 | path 17 | form 18 | query 19 | cookie 20 | header 21 | protobuf 22 | json 23 | raw_body 24 | default_val 25 | maxIn 26 | ) 27 | 28 | var ( 29 | allIn = func() []in { 30 | a := []in{} 31 | for i := undefined + 1; i < maxIn; i++ { 32 | a = append(a, i) 33 | } 34 | return a 35 | }() 36 | sortedDefaultIn = func() []in { 37 | var a []in 38 | for i := undefined + 1; i < raw_body; i++ { 39 | a = append(a, i) 40 | } 41 | return a 42 | }() 43 | ) 44 | 45 | type codec in 46 | 47 | const ( 48 | bodyUnsupport = codec(0) 49 | bodyForm = codec(form) 50 | bodyJSON = codec(json) 51 | bodyProtobuf = codec(protobuf) 52 | ) 53 | 54 | type receiver struct { 55 | hasPath, hasQuery, hasForm, hasJson, hasProtobuf, hasRawBody, hasHeader, hasCookie, hasDefaultVal, hasVd bool 56 | 57 | params []*paramInfo 58 | 59 | looseZeroMode bool 60 | } 61 | 62 | func (r *receiver) assginIn(i in, v bool) { 63 | switch i { 64 | case path: 65 | r.hasPath = r.hasPath || v 66 | case query: 67 | r.hasQuery = r.hasQuery || v 68 | case form: 69 | r.hasForm = r.hasForm || v 70 | case json: 71 | r.hasJson = r.hasJson || v 72 | case protobuf: 73 | r.hasProtobuf = r.hasProtobuf || v 74 | case raw_body: 75 | r.hasRawBody = r.hasRawBody || v 76 | case cookie: 77 | r.hasCookie = r.hasCookie || v 78 | case header: 79 | r.hasHeader = r.hasHeader || v 80 | case default_val: 81 | r.hasDefaultVal = r.hasDefaultVal || v 82 | } 83 | } 84 | 85 | func (r *receiver) getParam(fieldSelector string) *paramInfo { 86 | for _, p := range r.params { 87 | if p.fieldSelector == fieldSelector { 88 | return p 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | func (r *receiver) getOrAddParam(fh *tagexpr.FieldHandler, bindErrFactory func(failField, msg string) error) *paramInfo { 95 | fieldSelector := fh.StringSelector() 96 | p := r.getParam(fieldSelector) 97 | if p != nil { 98 | return p 99 | } 100 | p = ¶mInfo{ 101 | fieldSelector: fieldSelector, 102 | structField: fh.StructField(), 103 | omitIns: make(map[in]bool, maxIn), 104 | bindErrFactory: bindErrFactory, 105 | looseZeroMode: r.looseZeroMode, 106 | } 107 | r.params = append(r.params, p) 108 | return p 109 | } 110 | 111 | func (r *receiver) hasBody() bool { 112 | return r.hasForm || r.hasProtobuf || r.hasJson || r.hasRawBody 113 | } 114 | 115 | func (r *receiver) getBodyInfo(req Request) ( 116 | bodyCodec codec, bodyBytes []byte, postForm url.Values, 117 | fileHeaders map[string][]*multipart.FileHeader, err error, 118 | ) { 119 | bodyCodec = bodyUnsupport 120 | if !r.hasBody() { 121 | return 122 | } 123 | bodyCodec = getBodyCodec(req) 124 | bodyBytes, err = req.GetBody() 125 | if err == nil && r.hasForm { 126 | postForm, err = req.GetPostForm() 127 | if err == nil { 128 | if _req, ok := req.(requestWithFileHeader); ok { 129 | fileHeaders, err = _req.GetFileHeaders() 130 | } 131 | } 132 | } 133 | return 134 | } 135 | 136 | func (b *Binding) prebindBody(pointer interface{}, val reflect.Value, bodyCodec codec, bodyBytes []byte) error { 137 | switch bodyCodec { 138 | case bodyJSON: 139 | return b.bindJSON(pointer, bodyBytes) 140 | case bodyProtobuf: 141 | return bindProtobuf(pointer, bodyBytes) 142 | default: 143 | return nil 144 | } 145 | } 146 | 147 | const ( 148 | defaultMaxMemory = 32 << 20 // 32 MB 149 | ) 150 | 151 | func (r *receiver) getQuery(req Request) url.Values { 152 | if r.hasQuery { 153 | return req.GetQuery() 154 | } 155 | return nil 156 | } 157 | 158 | func (r *receiver) getCookies(req Request) []*http.Cookie { 159 | if r.hasCookie { 160 | return req.GetCookies() 161 | } 162 | return nil 163 | } 164 | 165 | func (r *receiver) getHeader(req Request) http.Header { 166 | if r.hasHeader { 167 | return req.GetHeader() 168 | } 169 | return nil 170 | } 171 | 172 | func (r *receiver) initParams() { 173 | names := make(map[string][maxIn]string, len(r.params)) 174 | for _, p := range r.params { 175 | if p.structField.Anonymous { 176 | continue 177 | } 178 | a := [maxIn]string{} 179 | for _, paramIn := range allIn { 180 | a[paramIn] = p.name(paramIn) 181 | } 182 | names[p.fieldSelector] = a 183 | } 184 | 185 | for _, p := range r.params { 186 | paths, _ := tagexpr.FieldSelector(p.fieldSelector).Split() 187 | for _, info := range p.tagInfos { 188 | var fs string 189 | for _, s := range paths { 190 | if fs == "" { 191 | fs = s 192 | } else { 193 | fs = tagexpr.JoinFieldSelector(fs, s) 194 | } 195 | name := names[fs][info.paramIn] 196 | if name != "" { 197 | info.namePath += name + "." 198 | } 199 | } 200 | info.namePath = info.namePath + p.name(info.paramIn) 201 | info.requiredError = p.bindErrFactory(info.namePath, "missing required parameter") 202 | info.typeError = p.bindErrFactory(info.namePath, "parameter type does not match binding data") 203 | info.cannotError = p.bindErrFactory(info.namePath, "parameter cannot be bound") 204 | info.contentTypeError = p.bindErrFactory(info.namePath, "does not support binding to the content type body") 205 | } 206 | p.setDefaultVal() 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /spec_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Bytedance Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tagexpr 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "reflect" 21 | "testing" 22 | ) 23 | 24 | func TestReadPairedSymbol(t *testing.T) { 25 | var cases = []struct { 26 | left, right rune 27 | expr, val, lastExprNode string 28 | }{ 29 | {left: '\'', right: '\'', expr: "'true '+'a'", val: "true ", lastExprNode: "+'a'"}, 30 | {left: '(', right: ')', expr: "((0+1)/(2-1)*9)%2", val: "(0+1)/(2-1)*9", lastExprNode: "%2"}, 31 | {left: '(', right: ')', expr: `(\)\(\))`, val: `)()`}, 32 | {left: '\'', right: '\'', expr: `'\\'`, val: `\\`}, 33 | {left: '\'', right: '\'', expr: `'\'\''`, val: `''`}, 34 | } 35 | for _, c := range cases { 36 | t.Log(c.expr) 37 | expr := c.expr 38 | got := readPairedSymbol(&expr, c.left, c.right) 39 | if got == nil { 40 | t.Fatalf("expr: %q, got: %v, %q, want: %q, %q", c.expr, got, expr, c.val, c.lastExprNode) 41 | } else if *got != c.val || expr != c.lastExprNode { 42 | t.Fatalf("expr: %q, got: %q, %q, want: %q, %q", c.expr, *got, expr, c.val, c.lastExprNode) 43 | } 44 | } 45 | } 46 | 47 | func TestReadBoolExprNode(t *testing.T) { 48 | var cases = []struct { 49 | expr string 50 | val bool 51 | lastExprNode string 52 | }{ 53 | {expr: "false", val: false, lastExprNode: ""}, 54 | {expr: "true", val: true, lastExprNode: ""}, 55 | {expr: "true ", val: true, lastExprNode: " "}, 56 | {expr: "!true&", val: false, lastExprNode: "&"}, 57 | {expr: "!false|", val: true, lastExprNode: "|"}, 58 | {expr: "!!!!false =", val: !!!!false, lastExprNode: " ="}, 59 | } 60 | for _, c := range cases { 61 | t.Log(c.expr) 62 | expr := c.expr 63 | e := readBoolExprNode(&expr) 64 | got := e.Run(context.TODO(), "", nil).(bool) 65 | if got != c.val || expr != c.lastExprNode { 66 | t.Fatalf("expr: %s, got: %v, %s, want: %v, %s", c.expr, got, expr, c.val, c.lastExprNode) 67 | } 68 | } 69 | } 70 | 71 | func TestReadDigitalExprNode(t *testing.T) { 72 | var cases = []struct { 73 | expr string 74 | val float64 75 | lastExprNode string 76 | }{ 77 | {expr: "0.1 +1", val: 0.1, lastExprNode: " +1"}, 78 | {expr: "-1\\1", val: -1, lastExprNode: "\\1"}, 79 | {expr: "1a", val: 0, lastExprNode: ""}, 80 | {expr: "1", val: 1, lastExprNode: ""}, 81 | {expr: "1.1", val: 1.1, lastExprNode: ""}, 82 | {expr: "1.1/", val: 1.1, lastExprNode: "/"}, 83 | } 84 | for _, c := range cases { 85 | expr := c.expr 86 | e := readDigitalExprNode(&expr) 87 | if c.expr == "1a" { 88 | if e != nil { 89 | t.Fatalf("expr: %s, got:%v, want:%v", c.expr, e.Run(context.TODO(), "", nil), nil) 90 | } 91 | continue 92 | } 93 | got := e.Run(context.TODO(), "", nil).(float64) 94 | if got != c.val || expr != c.lastExprNode { 95 | t.Fatalf("expr: %s, got: %f, %s, want: %f, %s", c.expr, got, expr, c.val, c.lastExprNode) 96 | } 97 | } 98 | } 99 | 100 | func TestFindSelector(t *testing.T) { 101 | var cases = []struct { 102 | expr string 103 | field string 104 | name string 105 | subSelector []string 106 | boolOpposite bool 107 | signOpposite bool 108 | found bool 109 | last string 110 | }{ 111 | {expr: "$", name: "$", found: true}, 112 | {expr: "!!$", name: "$", found: true}, 113 | {expr: "!$", name: "$", boolOpposite: true, found: true}, 114 | {expr: "+$", name: "$", found: true}, 115 | {expr: "--$", name: "$", found: true}, 116 | {expr: "-$", name: "$", signOpposite: true, found: true}, 117 | {expr: "---$", name: "$", signOpposite: true, found: true}, 118 | {expr: "()$", last: "()$"}, 119 | {expr: "(0)$", last: "(0)$"}, 120 | {expr: "(A)$", field: "A", name: "$", found: true}, 121 | {expr: "+(A)$", field: "A", name: "$", found: true}, 122 | {expr: "++(A)$", field: "A", name: "$", found: true}, 123 | {expr: "!(A)$", field: "A", name: "$", boolOpposite: true, found: true}, 124 | {expr: "-(A)$", field: "A", name: "$", signOpposite: true, found: true}, 125 | {expr: "(A0)$", field: "A0", name: "$", found: true}, 126 | {expr: "!!(A0)$", field: "A0", name: "$", found: true}, 127 | {expr: "--(A0)$", field: "A0", name: "$", found: true}, 128 | {expr: "(A0)$(A1)$", last: "(A0)$(A1)$"}, 129 | {expr: "(A0)$ $(A1)$", field: "A0", name: "$", found: true, last: " $(A1)$"}, 130 | {expr: "$a", last: "$a"}, 131 | {expr: "$[1]['a']", name: "$", subSelector: []string{"1", "'a'"}, found: true}, 132 | {expr: "$[1][]", last: "$[1][]"}, 133 | {expr: "$[[]]", last: "$[[]]"}, 134 | {expr: "$[[[]]]", last: "$[[[]]]"}, 135 | {expr: "$[(A)$[1]]", name: "$", subSelector: []string{"(A)$[1]"}, found: true}, 136 | {expr: "$>0&&$<10", name: "$", found: true, last: ">0&&$<10"}, 137 | } 138 | for _, c := range cases { 139 | last := c.expr 140 | field, name, subSelector, boolOpposite, signOpposite, found := findSelector(&last) 141 | if found != c.found { 142 | t.Fatalf("%q found: got: %v, want: %v", c.expr, found, c.found) 143 | } 144 | if c.boolOpposite && (boolOpposite == nil || !*boolOpposite) { 145 | t.Fatalf("%q boolOpposite: got: %v, want: %v", c.expr, boolOpposite, c.boolOpposite) 146 | } 147 | if c.signOpposite && (signOpposite == nil || !*signOpposite) { 148 | t.Fatalf("%q signOpposite: got: %v, want: %v", c.expr, signOpposite, c.signOpposite) 149 | } 150 | if field != c.field { 151 | t.Fatalf("%q field: got: %q, want: %q", c.expr, field, c.field) 152 | } 153 | if name != c.name { 154 | t.Fatalf("%q name: got: %q, want: %q", c.expr, name, c.name) 155 | } 156 | if !reflect.DeepEqual(subSelector, c.subSelector) { 157 | t.Fatalf("%q subSelector: got: %v, want: %v", c.expr, subSelector, c.subSelector) 158 | } 159 | if last != c.last { 160 | t.Fatalf("%q last: got: %q, want: %q", c.expr, last, c.last) 161 | } 162 | } 163 | } 164 | func printBoolPtr(b *bool) string { 165 | var v interface{} = b 166 | if b != nil { 167 | v = *b 168 | } 169 | return fmt.Sprint(v) 170 | } 171 | -------------------------------------------------------------------------------- /binding/gjson/gjson_test.go: -------------------------------------------------------------------------------- 1 | package gjson 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/rand" 7 | "reflect" 8 | "sync" 9 | "testing" 10 | 11 | "github.com/andeya/ameda" 12 | "github.com/stretchr/testify/assert" 13 | 14 | "github.com/bytedance/go-tagexpr/v2/binding/gjson/internal/rt" 15 | ) 16 | 17 | func TestMap(t *testing.T) { 18 | type X struct { 19 | M1 map[string]interface{} 20 | M2 map[string]struct { 21 | A string 22 | B int 23 | } 24 | M3 map[string]*struct { 25 | A string 26 | B int 27 | } 28 | } 29 | x := X{ 30 | M1: map[string]interface{}{"i": float64(9), "j": "*"}, 31 | M2: map[string]struct { 32 | A string 33 | B int 34 | }{"k2": {"a2", 12}}, 35 | M3: map[string]*struct { 36 | A string 37 | B int 38 | }{"k3": {"a2", 13}}, 39 | } 40 | data, _ := json.MarshalIndent(x, "", " ") 41 | t.Log(string(data)) 42 | 43 | var x2 X 44 | 45 | err := Unmarshal(data, &x2) 46 | assert.NoError(t, err) 47 | assert.Equal(t, x, x2) 48 | 49 | data = []byte(`{ 50 | "M1": { 51 | "i": 9, 52 | "j": "*" 53 | }, 54 | "M2": { 55 | "k2": { 56 | "A": "a2", 57 | "B": 12 58 | } 59 | }, 60 | "M3": { 61 | "k3": { 62 | "A": "a2", 63 | "B": "13" 64 | } 65 | } 66 | }`) 67 | 68 | var x3 *X 69 | err = Unmarshal(data, &x3) 70 | assert.NoError(t, err) 71 | assert.Equal(t, x, *x3) 72 | } 73 | 74 | func TestStruct(t *testing.T) { 75 | type a struct { 76 | V int `json:"v"` 77 | } 78 | type B struct { 79 | a 80 | A2 **a 81 | } 82 | type C struct { 83 | *B `json:"b"` 84 | } 85 | type D struct { 86 | *C `json:","` 87 | C2 *int 88 | } 89 | type E struct { 90 | D 91 | K int `json:"k"` 92 | int 93 | } 94 | data := []byte(`{ 95 | "k":1, 96 | "C2":null, 97 | "b":{"v":2,"A2":{"v":3}} 98 | }`) 99 | std := &E{} 100 | err := json.Unmarshal(data, std) 101 | if assert.NoError(t, err) { 102 | assert.Equal(t, 1, std.K) 103 | assert.Equal(t, 2, std.V) 104 | assert.Equal(t, 3, (*std.A2).V) 105 | } 106 | g := &E{} 107 | err = Unmarshal(data, g) 108 | assert.NoError(t, err) 109 | assert.Equal(t, std, g) 110 | 111 | type X struct { 112 | *X 113 | Y int 114 | } 115 | data2 := []byte(`{"X":{"Y":2}}`) 116 | std2 := &X{} 117 | err = json.Unmarshal(data2, std2) 118 | if assert.NoError(t, err) { 119 | t.Logf("%#v", std2) 120 | } 121 | g2 := &X{} 122 | err = Unmarshal(data2, g2) 123 | assert.NoError(t, err) 124 | assert.Equal(t, std2, g2) 125 | } 126 | 127 | func TestAliasBUG1(t *testing.T) { 128 | type DeviceUUID string 129 | type DeviceUUIDMap map[DeviceUUID]string 130 | type AttachedMobiles struct { 131 | AttachedAndroid DeviceUUIDMap `json:"android,omitempty"` 132 | AttachedIOS DeviceUUIDMap `json:"ios,omitempty"` 133 | } 134 | b, err := json.MarshalIndent(ameda.InitSampleValue(reflect.TypeOf(AttachedMobiles{}), 10).Interface(), "", " ") 135 | assert.NoError(t, err) 136 | var r AttachedMobiles 137 | err = Unmarshal(b, &r) 138 | assert.NoError(t, err) 139 | // b, err = json.Marshal(map[float32]int{ 140 | // 1.0: 4, 141 | // }) 142 | // assert.NoError(t, err) 143 | // t.Log(string(b)) 144 | } 145 | 146 | func TestBingSliceWithObject(t *testing.T) { 147 | type F struct { 148 | UID int64 149 | } 150 | type foo struct { 151 | F1 []F `json:"f1"` 152 | F2 []F `json:"f2"` 153 | } 154 | str := `{"f1":{"UID":1},"f2":[{"UID":"2233"}]}` 155 | 156 | obj := foo{} 157 | err := Unmarshal([]byte(str), &obj) 158 | 159 | assert.NoError(t, err) 160 | assert.Len(t, obj.F1, 0) 161 | } 162 | func BenchmarkGetFiledInfo(b *testing.B) { 163 | var types []reflect.Type 164 | const count = 2000 165 | for i := 0; i < count; i++ { 166 | xtype := genStruct(i) 167 | 168 | getFiledInfo(xtype) 169 | 170 | types = append(types, xtype) 171 | } 172 | b.ResetTimer() 173 | b.RunParallel(func(pb *testing.PB) { 174 | var i int64 175 | for pb.Next() { 176 | getFiledInfo(types[i%count]) 177 | i++ 178 | } 179 | }) 180 | } 181 | func BenchmarkGetFieldInfoByMap(b *testing.B) { 182 | var types []reflect.Type 183 | const count = 2000 184 | for i := 0; i < count; i++ { 185 | xtype := genStruct(i) 186 | 187 | getFiledInfoWithMap(xtype) 188 | 189 | types = append(types, xtype) 190 | } 191 | b.ResetTimer() 192 | b.RunParallel(func(pb *testing.PB) { 193 | var i int64 194 | for pb.Next() { 195 | getFiledInfoWithMap(types[i%count]) 196 | i++ 197 | } 198 | }) 199 | } 200 | 201 | func genStruct(n int) reflect.Type { 202 | numOfFields := rand.Intn(50) + 1 203 | field := make([]reflect.StructField, 0, numOfFields) 204 | for i := 0; i < numOfFields; i++ { 205 | field = append(field, reflect.StructField{ 206 | Name: fmt.Sprintf("F%d_%d", n, i), 207 | PkgPath: "", 208 | Type: reflect.TypeOf(struct { 209 | A int 210 | B map[int]interface{} 211 | }{}), 212 | }) 213 | } 214 | ot := reflect.StructOf(field) 215 | return ot 216 | } 217 | 218 | var fieldsmu sync.RWMutex 219 | var fields = make(map[uintptr]map[string][]int) 220 | 221 | func getFiledInfoWithMap(t reflect.Type) map[string][]int { 222 | 223 | runtimeTypeID := ameda.RuntimeTypeID(t) 224 | fieldsmu.RLock() 225 | sf := fields[runtimeTypeID] 226 | fieldsmu.RUnlock() 227 | if sf == nil { 228 | fieldsmu.Lock() 229 | defer fieldsmu.Unlock() 230 | 231 | d := rt.UnpackType(t) 232 | sf1, _ := computeTypeInfo(d) 233 | sf = sf1.(map[string][]int) 234 | fields[runtimeTypeID] = sf 235 | 236 | } 237 | return sf 238 | } 239 | 240 | // MarshalJSON to output non base64 encoded []byte 241 | func (j ByteSlice) MarshalJSON() ([]byte, error) { 242 | if len(j) == 0 { 243 | return []byte("null"), nil 244 | } 245 | 246 | return json.RawMessage(j).MarshalJSON() 247 | } 248 | 249 | // UnmarshalJSON to deserialize []byte 250 | func (j *ByteSlice) UnmarshalJSON(b []byte) error { 251 | result := json.RawMessage{} 252 | err := result.UnmarshalJSON(b) 253 | *j = ByteSlice(result) 254 | return err 255 | } 256 | 257 | type ByteSlice []byte 258 | 259 | func TestCustomizedGjsonUnmarshal(t *testing.T) { 260 | str := `{"h1":{"h2":1}}` 261 | type F struct { 262 | H ByteSlice `json:"h1"` 263 | } 264 | 265 | obj := F{} 266 | err := Unmarshal([]byte(str), &obj) 267 | 268 | assert.NoError(t, err) 269 | assert.Equal(t, "{\"h2\":1}", string(obj.H)) 270 | 271 | obj2 := F{} 272 | err = json.Unmarshal([]byte(str), &obj2) 273 | assert.NoError(t, err) 274 | assert.Equal(t, "{\"h2\":1}", string(obj2.H)) 275 | 276 | assert.Equal(t, obj.H, obj2.H) 277 | } 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-tagexpr [![report card](https://goreportcard.com/badge/github.com/bytedance/go-tagexpr?style=flat-square)](http://goreportcard.com/report/bytedance/go-tagexpr) [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/bytedance/go-tagexpr) 2 | 3 | An interesting go struct tag expression syntax for field validation, etc. 4 | 5 | ## Usage 6 | 7 | - **[Validator](https://github.com/bytedance/go-tagexpr/tree/master/validator)**: A powerful validator that supports struct tag expression 8 | 9 | - **[Binding](https://github.com/bytedance/go-tagexpr/tree/master/binding)**: A powerful HTTP request parameters binder that supports struct tag expression 10 | 11 | ## Feature 12 | 13 | - Support for a variety of common operator 14 | - Support for accessing arrays, slices, members of the dictionary 15 | - Support access to any field in the current structure 16 | - Support access to nested fields, non-exported fields, etc. 17 | - Support variable 18 | - Support registers function expression 19 | - Built-in len, sprintf, regexp functions 20 | - Support single mode and multiple mode to define expression 21 | - Parameter check subpackage 22 | - Use offset pointers to directly take values, better performance 23 | - Required go version ≥1.9 24 | 25 | ## Example 26 | 27 | ```go 28 | package tagexpr_test 29 | 30 | import ( 31 | "fmt" 32 | 33 | tagexpr "github.com/bytedance/go-tagexpr/v2" 34 | ) 35 | 36 | func Example() { 37 | type T struct { 38 | A int `tagexpr:"$<0||$>=100"` 39 | B string `tagexpr:"len($)>1 && regexp('^\\w*$')"` 40 | C bool `tagexpr:"expr1:(f.g)$>0 && $; expr2:'C must be true when T.f.g>0'"` 41 | d []string `tagexpr:"@:len($)>0 && $[0]=='D'; msg:sprintf('invalid d: %v',$)"` 42 | e map[string]int `tagexpr:"len($)==$['len']"` 43 | e2 map[string]*int `tagexpr:"len($)==$['len']"` 44 | f struct { 45 | g int `tagexpr:"$"` 46 | } 47 | h int `tagexpr:"$>minVal"` 48 | } 49 | 50 | vm := tagexpr.New("tagexpr") 51 | t := &T{ 52 | A: 107, 53 | B: "abc", 54 | C: true, 55 | d: []string{"x", "y"}, 56 | e: map[string]int{"len": 1}, 57 | e2: map[string]*int{"len": new(int)}, 58 | f: struct { 59 | g int `tagexpr:"$"` 60 | }{1}, 61 | h: 10, 62 | } 63 | 64 | tagExpr, err := vm.Run(t) 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | fmt.Println(tagExpr.Eval("A")) 70 | fmt.Println(tagExpr.Eval("B")) 71 | fmt.Println(tagExpr.Eval("C@expr1")) 72 | fmt.Println(tagExpr.Eval("C@expr2")) 73 | if !tagExpr.Eval("d").(bool) { 74 | fmt.Println(tagExpr.Eval("d@msg")) 75 | } 76 | fmt.Println(tagExpr.Eval("e")) 77 | fmt.Println(tagExpr.Eval("e2")) 78 | fmt.Println(tagExpr.Eval("f.g")) 79 | fmt.Println(tagExpr.EvalWithEnv("h", map[string]interface{}{"minVal": 9})) 80 | fmt.Println(tagExpr.EvalWithEnv("h", map[string]interface{}{"minVal": 11})) 81 | 82 | // Output: 83 | // true 84 | // true 85 | // true 86 | // C must be true when T.f.g>0 87 | // invalid d: [x y] 88 | // true 89 | // false 90 | // 1 91 | // true 92 | // false 93 | } 94 | ``` 95 | 96 | ## Syntax 97 | 98 | Struct tag syntax spec: 99 | 100 | ``` 101 | type T struct { 102 | // Single model 103 | Field1 T1 `tagName:"expression"` 104 | // Multiple model 105 | Field2 T2 `tagName:"exprName:expression; [exprName2:expression2;]..."` 106 | // Omit it 107 | Field3 T3 `tagName:"-"` 108 | // Omit it when it is nil 109 | Field4 T4 `tagName:"?"` 110 | ... 111 | } 112 | ``` 113 | 114 | NOTE: **The `exprName` under the same struct field cannot be the same!** 115 | 116 | |Operator or Operand|Explain| 117 | |-----|---------| 118 | |`true` `false`|boolean| 119 | |`0` `0.0`|float64 "0"| 120 | |`''`|String| 121 | |`\\'`| Escape `'` delims in string| 122 | |`\"`| Escape `"` delims in string| 123 | |`nil`|nil, undefined| 124 | |`!`|not| 125 | |`+`|Digital addition or string splicing| 126 | |`-`|Digital subtraction or negative| 127 | |`*`|Digital multiplication| 128 | |`/`|Digital division| 129 | |`%`|division remainder, as: `float64(int64(a)%int64(b))`| 130 | |`==`|`eq`| 131 | |`!=`|`ne`| 132 | |`>`|`gt`| 133 | |`>=`|`ge`| 134 | |`<`|`lt`| 135 | |`<=`|`le`| 136 | |`&&`|Logic `and`| 137 | |`\|\|`|Logic `or`| 138 | |`()`|Expression group| 139 | |`(X)$`|Struct field value named X| 140 | |`(X.Y)$`|Struct field value named X.Y| 141 | |`$`|Shorthand for `(X)$`, omit `(X)` to indicate current struct field value| 142 | |`(X)$['A']`|Map value with key A or struct A sub-field in the struct field X| 143 | |`(X)$[0]`|The 0th element or sub-field of the struct field X(type: map, slice, array, struct)| 144 | |`len((X)$)`|Built-in function `len`, the length of struct field X| 145 | |`mblen((X)$)`|the length of string field X (character number)| 146 | |`regexp('^\\w*$', (X)$)`|Regular match the struct field X, return boolean| 147 | |`regexp('^\\w*$')`|Regular match the current struct field, return boolean| 148 | |`sprintf('X value: %v', (X)$)`|`fmt.Sprintf`, format the value of struct field X| 149 | |`range(KvExpr, forEachExpr)`|Iterate over an array, slice, or dictionary
- `#k` is the element key var
- `#v` is the element value var
- `##` is the number of elements
- e.g. [example](spec_range_test.go)| 150 | |`in((X)$, enum_1, ...enum_n)`|Check if the first parameter is one of the enumerated parameters| 151 | 152 | 154 | 155 | 161 | 162 | Operator priority(high -> low): 163 | 164 | * `()` `!` `bool` `float64` `string` `nil` 165 | * `*` `/` `%` 166 | * `+` `-` 167 | * `<` `<=` `>` `>=` 168 | * `==` `!=` 169 | * `&&` 170 | * `||` 171 | 172 | ## Field Selector 173 | 174 | ``` 175 | field_lv1.field_lv2...field_lvn 176 | ``` 177 | 178 | ## Expression Selector 179 | 180 | - If expression is **single model** or exprName is `@`: 181 | 182 | ``` 183 | field_lv1.field_lv2...field_lvn 184 | ``` 185 | 186 | - If expression is **multiple model** and exprName is not `@`: 187 | 188 | ``` 189 | field_lv1.field_lv2...field_lvn@exprName 190 | ``` 191 | 192 | ## Benchmark 193 | 194 | ``` 195 | goos: darwin 196 | goarch: amd64 197 | pkg: github.com/bytedance/go-tagexpr 198 | BenchmarkTagExpr-4 10000000 148 ns/op 32 B/op 3 allocs/op 199 | BenchmarkReflect-4 10000000 182 ns/op 16 B/op 2 allocs/op 200 | PASS 201 | ``` 202 | 203 | [Go to test code](https://github.com/bytedance/go-tagexpr/blob/master/tagexpr_test.go#L9-L56) 204 | -------------------------------------------------------------------------------- /validator/README.md: -------------------------------------------------------------------------------- 1 | # validator [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/bytedance/go-tagexpr/v2/validator) 2 | 3 | A powerful validator that supports struct tag expression. 4 | 5 | ## Feature 6 | 7 | - Support for a variety of common operator 8 | - Support for accessing arrays, slices, members of the dictionary 9 | - Support access to any field in the current structure 10 | - Support access to nested fields, non-exported fields, etc. 11 | - Support registers validator function expression 12 | - Built-in len, sprintf, regexp, email, phone functions 13 | - Support simple mode, or specify error message mode 14 | - Use offset pointers to directly take values, better performance 15 | - Required go version ≥1.9 16 | 17 | ## Example 18 | 19 | ```go 20 | package validator_test 21 | 22 | import ( 23 | "fmt" 24 | 25 | vd "github.com/bytedance/go-tagexpr/v2/validator" 26 | ) 27 | 28 | func Example() { 29 | type InfoRequest struct { 30 | Name string `vd:"($!='Alice'||(Age)$==18) && regexp('\\w')"` 31 | Age int `vd:"$>0"` 32 | Email string `vd:"email($)"` 33 | Phone1 string `vd:"phone($)"` 34 | OtherPhones []string `vd:"range($, phone(#v,'CN'))"` 35 | *InfoRequest `vd:"?"` 36 | Info1 *InfoRequest `vd:"?"` 37 | Info2 *InfoRequest `vd:"-"` 38 | } 39 | info := &InfoRequest{ 40 | Name: "Alice", 41 | Age: 18, 42 | Email: "henrylee2cn@gmail.com", 43 | Phone1: "+8618812345678", 44 | OtherPhones: []string{"18812345679", "18812345680"}, 45 | } 46 | fmt.Println(vd.Validate(info)) 47 | 48 | type A struct { 49 | A int `vd:"$<0||$>=100"` 50 | Info interface{} 51 | } 52 | info.Email = "xxx" 53 | a := &A{A: 107, Info: info} 54 | fmt.Println(vd.Validate(a)) 55 | type B struct { 56 | B string `vd:"len($)>1 && regexp('^\\w*$')"` 57 | } 58 | b := &B{"abc"} 59 | fmt.Println(vd.Validate(b) == nil) 60 | 61 | type C struct { 62 | C bool `vd:"@:(S.A)$>0 && !$; msg:'C must be false when S.A>0'"` 63 | S *A 64 | } 65 | c := &C{C: true, S: a} 66 | fmt.Println(vd.Validate(c)) 67 | 68 | type D struct { 69 | d []string `vd:"@:len($)>0 && $[0]=='D'; msg:sprintf('invalid d: %v',$)"` 70 | } 71 | d := &D{d: []string{"x", "y"}} 72 | fmt.Println(vd.Validate(d)) 73 | 74 | type E struct { 75 | e map[string]int `vd:"len($)==$['len']"` 76 | } 77 | e := &E{map[string]int{"len": 2}} 78 | fmt.Println(vd.Validate(e)) 79 | 80 | // Customizes the factory of validation error. 81 | vd.SetErrorFactory(func(failPath, msg string) error { 82 | return fmt.Errorf(`{"succ":false, "error":"validation failed: %s"}`, failPath) 83 | }) 84 | 85 | type F struct { 86 | f struct { 87 | g int `vd:"$%3==0"` 88 | } 89 | } 90 | f := &F{} 91 | f.f.g = 10 92 | fmt.Println(vd.Validate(f)) 93 | 94 | fmt.Println(vd.Validate(map[string]*F{"a": f})) 95 | fmt.Println(vd.Validate(map[string]map[string]*F{"a": {"b": f}})) 96 | fmt.Println(vd.Validate([]map[string]*F{{"a": f}})) 97 | fmt.Println(vd.Validate(struct { 98 | A []map[string]*F 99 | }{A: []map[string]*F{{"x": f}}})) 100 | fmt.Println(vd.Validate(map[*F]int{f: 1})) 101 | fmt.Println(vd.Validate([][1]*F{{f}})) 102 | fmt.Println(vd.Validate((*F)(nil))) 103 | fmt.Println(vd.Validate(map[string]*F{})) 104 | fmt.Println(vd.Validate(map[string]map[string]*F{})) 105 | fmt.Println(vd.Validate([]map[string]*F{})) 106 | fmt.Println(vd.Validate([]*F{})) 107 | 108 | // Output: 109 | // 110 | // email format is incorrect 111 | // true 112 | // C must be false when S.A>0 113 | // invalid d: [x y] 114 | // invalid parameter: e 115 | // {"succ":false, "error":"validation failed: f.g"} 116 | // {"succ":false, "error":"validation failed: {v for k=a}.f.g"} 117 | // {"succ":false, "error":"validation failed: {v for k=a}{v for k=b}.f.g"} 118 | // {"succ":false, "error":"validation failed: [0]{v for k=a}.f.g"} 119 | // {"succ":false, "error":"validation failed: A[0]{v for k=x}.f.g"} 120 | // {"succ":false, "error":"validation failed: {k}.f.g"} 121 | // {"succ":false, "error":"validation failed: [0][0].f.g"} 122 | // unsupport data: nil 123 | // 124 | // 125 | // 126 | // 127 | } 128 | ``` 129 | 130 | ## Syntax 131 | 132 | Struct tag syntax spec: 133 | 134 | ``` 135 | type T struct { 136 | // Simple model 137 | Field1 T1 `tagName:"expression"` 138 | // Specify error message mode 139 | Field2 T2 `tagName:"@:expression; msg:expression2"` 140 | // Omit it 141 | Field3 T3 `tagName:"-"` 142 | // Omit it when it is nil 143 | Field4 T4 `tagName:"?"` 144 | ... 145 | } 146 | ``` 147 | 148 | |Operator or Operand|Explain| 149 | |-----|---------| 150 | |`true` `false`|boolean| 151 | |`0` `0.0`|float64 "0"| 152 | |`''`|String| 153 | |`\\'`| Escape `'` delims in string| 154 | |`\"`| Escape `"` delims in string| 155 | |`nil`|nil, undefined| 156 | |`!`|not| 157 | |`+`|Digital addition or string splicing| 158 | |`-`|Digital subtraction or negative| 159 | |`*`|Digital multiplication| 160 | |`/`|Digital division| 161 | |`%`|division remainder, as: `float64(int64(a)%int64(b))`| 162 | |`==`|`eq`| 163 | |`!=`|`ne`| 164 | |`>`|`gt`| 165 | |`>=`|`ge`| 166 | |`<`|`lt`| 167 | |`<=`|`le`| 168 | |`&&`|Logic `and`| 169 | |`\|\|`|Logic `or`| 170 | |`()`|Expression group| 171 | |`(X)$`|Struct field value named X| 172 | |`(X.Y)$`|Struct field value named X.Y| 173 | |`$`|Shorthand for `(X)$`, omit `(X)` to indicate current struct field value| 174 | |`(X)$['A']`|Map value with key A or struct A sub-field in the struct field X| 175 | |`(X)$[0]`|The 0th element or sub-field of the struct field X(type: map, slice, array, struct)| 176 | |`len((X)$)`|Built-in function `len`, the length of struct field X| 177 | |`mblen((X)$)`|the length of string field X (character number)| 178 | |`regexp('^\\w*$', (X)$)`|Regular match the struct field X, return boolean| 179 | |`regexp('^\\w*$')`|Regular match the current struct field, return boolean| 180 | |`sprintf('X value: %v', (X)$)`|`fmt.Sprintf`, format the value of struct field X| 181 | |`range(KvExpr, forEachExpr)`|Iterate over an array, slice, or dictionary
- `#k` is the element key var
- `#v` is the element value var
- `##` is the number of elements
- e.g. [example](../spec_range_test.go)| 182 | |`in((X)$, enum_1, ...enum_n)`|Check if the first parameter is one of the enumerated parameters| 183 | |`email((X)$)`|Regular match the struct field X, return true if it is email| 184 | |`phone((X)$,<'defaultRegion'>)`|Regular match the struct field X, return true if it is phone| 185 | 186 | 188 | 189 | 195 | 196 | Operator priority(high -> low): 197 | 198 | * `()` `!` `bool` `float64` `string` `nil` 199 | * `*` `/` `%` 200 | * `+` `-` 201 | * `<` `<=` `>` `>=` 202 | * `==` `!=` 203 | * `&&` 204 | * `||` 205 | -------------------------------------------------------------------------------- /expr_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Bytedance Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tagexpr 16 | 17 | import ( 18 | "math" 19 | "reflect" 20 | "testing" 21 | ) 22 | 23 | func TestExpr(t *testing.T) { 24 | var cases = []struct { 25 | expr string 26 | val interface{} 27 | }{ 28 | // Simple string 29 | {expr: "'a'", val: "a"}, 30 | {expr: "('a')", val: "a"}, 31 | // Simple digital 32 | {expr: " 10 ", val: 10.0}, 33 | {expr: "(10)", val: 10.0}, 34 | // Simple bool 35 | {expr: "true", val: true}, 36 | {expr: "!true", val: false}, 37 | {expr: "!!true", val: true}, 38 | {expr: "false", val: false}, 39 | {expr: "!false", val: true}, 40 | {expr: "!!false", val: false}, 41 | {expr: "(false)", val: false}, 42 | {expr: "(!false)", val: true}, 43 | {expr: "(!!false)", val: false}, 44 | {expr: "!!(!false)", val: true}, 45 | {expr: "!(!false)", val: false}, 46 | // Join string 47 | {expr: "'true '+('a')", val: "true a"}, 48 | {expr: "'a'+('b'+'c')+'d'", val: "abcd"}, 49 | // Arithmetic operator 50 | {expr: "1+7+2", val: 10.0}, 51 | {expr: "1+(7)+(2)", val: 10.0}, 52 | {expr: "1.1+ 2", val: 3.1}, 53 | {expr: "-1.1+4", val: 2.9}, 54 | {expr: "10-7-2", val: 1.0}, 55 | {expr: "20/2", val: 10.0}, 56 | {expr: "1/0", val: math.NaN()}, 57 | {expr: "20%2", val: 0.0}, 58 | {expr: "6 % 5", val: 1.0}, 59 | {expr: "20%7 %5", val: 1.0}, 60 | {expr: "1*2+7+2.2", val: 11.2}, 61 | {expr: "-20/2+1+2", val: -7.0}, 62 | {expr: "20/2+1-2-1", val: 8.0}, 63 | {expr: "30/(2+1)/5-2-1", val: -1.0}, 64 | {expr: "100/(( 2+8)*5 )-(1 +1- 0)", val: 0.0}, 65 | {expr: "(2*3)+(4*2)", val: 14.0}, 66 | {expr: "1+(2*(3+4))", val: 15.0}, 67 | {expr: "20%(7%5)", val: 0.0}, 68 | // Relational operator 69 | {expr: "50 == 5", val: false}, 70 | {expr: "'50'==50", val: true}, 71 | {expr: "'50'=='50'", val: true}, 72 | {expr: "'50' =='5' == true", val: false}, 73 | {expr: "50== 50 == false", val: false}, 74 | {expr: "50== 50 == true ==true==true", val: true}, 75 | {expr: "50 != 5", val: true}, 76 | {expr: "'50'!=50", val: false}, 77 | {expr: "'50'!= '50'", val: false}, 78 | {expr: "'50' !='5' != true", val: false}, 79 | {expr: "50!= 50 == false", val: true}, 80 | {expr: "50== 50 != true ==true!=true", val: true}, 81 | {expr: "50 > 5", val: true}, 82 | {expr: "50.1 > 50.1", val: false}, 83 | {expr: "3.2 > 2.1", val: true}, 84 | {expr: "'3.2' > '2.1'", val: true}, 85 | {expr: "'13.2'>'2.1'", val: false}, 86 | {expr: "3.2 >= 2.1", val: true}, 87 | {expr: "2.1 >= 2.1", val: true}, 88 | {expr: "2.05 >= 2.1", val: false}, 89 | {expr: "'2.05'>='2.1'", val: false}, 90 | {expr: "'12.05'>='2.1'", val: false}, 91 | {expr: "50 < 5", val: false}, 92 | {expr: "50.1 < 50.1", val: false}, 93 | {expr: "3 <12.11", val: true}, 94 | {expr: "3.2 < 2.1", val: false}, 95 | {expr: "'3.2' < '2.1'", val: false}, 96 | {expr: "'13.2' < '2.1'", val: true}, 97 | {expr: "3.2 <= 2.1", val: false}, 98 | {expr: "2.1 <= 2.1", val: true}, 99 | {expr: "2.05 <= 2.1", val: true}, 100 | {expr: "'2.05'<='2.1'", val: true}, 101 | {expr: "'12.05'<='2.1'", val: true}, 102 | // Logical operator 103 | {expr: "!('13.2' < '2.1')", val: false}, 104 | {expr: "(3.2 <= 2.1) &&true", val: false}, 105 | {expr: "true&&(2.1<=2.1)", val: true}, 106 | {expr: "(2.05<=2.1)&&false", val: false}, 107 | {expr: "true&&!true&&false", val: false}, 108 | {expr: "true&&true&&true", val: true}, 109 | {expr: "true&&true&&false", val: false}, 110 | {expr: "false&&true&&true", val: false}, 111 | {expr: "true && false && true", val: false}, 112 | {expr: "true||false", val: true}, 113 | {expr: "false ||true", val: true}, 114 | {expr: "true&&true || false", val: true}, 115 | {expr: "true&&false || false", val: false}, 116 | {expr: "true && false || true ", val: true}, 117 | } 118 | for _, c := range cases { 119 | t.Log(c.expr) 120 | vm, err := parseExpr(c.expr) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | val := vm.run("", nil) 125 | if !reflect.DeepEqual(val, c.val) { 126 | if f, ok := c.val.(float64); ok && math.IsNaN(f) && math.IsNaN(val.(float64)) { 127 | continue 128 | } 129 | t.Fatalf("expr: %q, got: %v, expect: %v", c.expr, val, c.val) 130 | } 131 | } 132 | } 133 | 134 | func TestExprWithEnv(t *testing.T) { 135 | var cases = []struct { 136 | expr string 137 | val interface{} 138 | }{ 139 | // env: a = 10, b = "string value", 140 | {expr: "a", val: 10.0}, 141 | {expr: "b", val: "string value"}, 142 | {expr: "a>10", val: false}, 143 | {expr: "a<11", val: true}, 144 | {expr: "a+1", val: 11.0}, 145 | {expr: "a==10", val: true}, 146 | } 147 | 148 | for _, c := range cases { 149 | t.Log(c.expr) 150 | vm, err := parseExpr(c.expr) 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | val := vm.runWithEnv("", nil, map[string]interface{}{"a": 10, "b": "string value"}) 155 | if !reflect.DeepEqual(val, c.val) { 156 | if f, ok := c.val.(float64); ok && math.IsNaN(f) && math.IsNaN(val.(float64)) { 157 | continue 158 | } 159 | t.Fatalf("expr: %q, got: %v, expect: %v", c.expr, val, c.val) 160 | } 161 | } 162 | } 163 | 164 | func TestPriority(t *testing.T) { 165 | var cases = []struct { 166 | expr string 167 | val interface{} 168 | }{ 169 | {expr: "false||true&&8==8", val: true}, 170 | {expr: "1+2>5-4", val: true}, 171 | {expr: "1+2*4/2", val: 5.0}, 172 | {expr: "(true||false)&&false||false", val: false}, 173 | {expr: "true||false&&false||false", val: true}, 174 | {expr: "true||1<0&&'a'!='a'||0!=0", val: true}, 175 | } 176 | for _, c := range cases { 177 | t.Log(c.expr) 178 | vm, err := parseExpr(c.expr) 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | val := vm.run("", nil) 183 | if !reflect.DeepEqual(val, c.val) { 184 | if f, ok := c.val.(float64); ok && math.IsNaN(f) && math.IsNaN(val.(float64)) { 185 | continue 186 | } 187 | t.Fatalf("expr: %q, got: %v, expect: %v", c.expr, val, c.val) 188 | } 189 | } 190 | } 191 | 192 | func TestBuiltInFunc(t *testing.T) { 193 | var cases = []struct { 194 | expr string 195 | val interface{} 196 | }{ 197 | {expr: "len('abc')", val: 3.0}, 198 | {expr: "len('abc')+2*2/len('cd')", val: 5.0}, 199 | {expr: "len(0)", val: 0.0}, 200 | 201 | {expr: "regexp('a\\d','a0')", val: true}, 202 | {expr: "regexp('^a\\d$','a0')", val: true}, 203 | {expr: "regexp('a\\d','a')", val: false}, 204 | {expr: "regexp('^a\\d$','a')", val: false}, 205 | 206 | {expr: "sprintf('test string: %s','a')", val: "test string: a"}, 207 | {expr: "sprintf('test string: %s','a'+'b')", val: "test string: ab"}, 208 | {expr: "sprintf('test string: %s,%v','a',1)", val: "test string: a,1"}, 209 | {expr: "sprintf('')+'a'", val: "a"}, 210 | {expr: "sprintf('%v',10+2*2)", val: "14"}, 211 | } 212 | for _, c := range cases { 213 | t.Log(c.expr) 214 | vm, err := parseExpr(c.expr) 215 | if err != nil { 216 | t.Fatal(err) 217 | } 218 | val := vm.run("", nil) 219 | if !reflect.DeepEqual(val, c.val) { 220 | if f, ok := c.val.(float64); ok && math.IsNaN(f) && math.IsNaN(val.(float64)) { 221 | continue 222 | } 223 | t.Fatalf("expr: %q, got: %v, expect: %v", c.expr, val, c.val) 224 | } 225 | } 226 | } 227 | 228 | func TestSyntaxIncorrect(t *testing.T) { 229 | var cases = []struct { 230 | incorrectExpr string 231 | }{ 232 | {incorrectExpr: "1 + + 'a'"}, 233 | {incorrectExpr: "regexp()"}, 234 | {incorrectExpr: "regexp('^'+'a','a')"}, 235 | {incorrectExpr: "regexp('^a','a','b')"}, 236 | {incorrectExpr: "sprintf()"}, 237 | {incorrectExpr: "sprintf(0)"}, 238 | {incorrectExpr: "sprintf('a'+'b')"}, 239 | } 240 | for _, c := range cases { 241 | _, err := parseExpr(c.incorrectExpr) 242 | if err == nil { 243 | t.Fatalf("expect syntax incorrect: %s", c.incorrectExpr) 244 | } else { 245 | t.Log(err) 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /expr.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Bytedance Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tagexpr 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/andeya/goutil" 24 | ) 25 | 26 | type variableKeyType string 27 | const variableKey variableKeyType = "__ENV_KEY__" 28 | 29 | // Expr expression 30 | type Expr struct { 31 | expr ExprNode 32 | } 33 | 34 | // parseExpr parses the expression. 35 | func parseExpr(expr string) (*Expr, error) { 36 | e := newGroupExprNode() 37 | p := &Expr{ 38 | expr: e, 39 | } 40 | s := expr 41 | err := p.parseExprNode(&s, e) 42 | if err != nil { 43 | return nil, err 44 | } 45 | sortPriority(e) 46 | return p, nil 47 | } 48 | func (p *Expr) parseExprNode(expr *string, e ExprNode) error { 49 | trimLeftSpace(expr) 50 | if *expr == "" { 51 | return nil 52 | } 53 | operand := p.readSelectorExprNode(expr) 54 | if operand == nil { 55 | operand = p.readRangeKvExprNode(expr) 56 | if operand == nil { 57 | var subExprNode *string 58 | operand, subExprNode = readGroupExprNode(expr) 59 | if operand != nil { 60 | err := p.parseExprNode(subExprNode, operand) 61 | if err != nil { 62 | return err 63 | } 64 | } else { 65 | operand = p.parseOperand(expr) 66 | } 67 | } 68 | } 69 | if operand == nil { 70 | return fmt.Errorf("syntax error: %q", *expr) 71 | } 72 | trimLeftSpace(expr) 73 | operator := p.parseOperator(expr) 74 | if operator == nil { 75 | e.SetRightOperand(operand) 76 | operand.SetParent(e) 77 | return nil 78 | } 79 | if _, ok := e.(*groupExprNode); ok { 80 | operator.SetLeftOperand(operand) 81 | operand.SetParent(operator) 82 | e.SetRightOperand(operator) 83 | operator.SetParent(e) 84 | } else { 85 | operator.SetParent(e.Parent()) 86 | operator.Parent().SetRightOperand(operator) 87 | operator.SetLeftOperand(e) 88 | e.SetParent(operator) 89 | e.SetRightOperand(operand) 90 | operand.SetParent(e) 91 | } 92 | return p.parseExprNode(expr, operator) 93 | } 94 | 95 | func (p *Expr) parseOperand(expr *string) (e ExprNode) { 96 | for _, fn := range funcList { 97 | if e = fn(p, expr); e != nil { 98 | return e 99 | } 100 | } 101 | if e = readStringExprNode(expr); e != nil { 102 | return e 103 | } 104 | if e = readDigitalExprNode(expr); e != nil { 105 | return e 106 | } 107 | if e = readBoolExprNode(expr); e != nil { 108 | return e 109 | } 110 | if e = readNilExprNode(expr); e != nil { 111 | return e 112 | } 113 | if e = readVariableExprNode(expr); e != nil { 114 | return e 115 | } 116 | return nil 117 | } 118 | 119 | func (*Expr) parseOperator(expr *string) (e ExprNode) { 120 | s := *expr 121 | if len(s) < 2 { 122 | return nil 123 | } 124 | defer func() { 125 | if e != nil && *expr == s { 126 | *expr = (*expr)[2:] 127 | } 128 | }() 129 | a := s[:2] 130 | switch a { 131 | // case "<<": 132 | // case ">>": 133 | // case "&^": 134 | case "||": 135 | return newOrExprNode() 136 | case "&&": 137 | return newAndExprNode() 138 | case "==": 139 | return newEqualExprNode() 140 | case ">=": 141 | return newGreaterEqualExprNode() 142 | case "<=": 143 | return newLessEqualExprNode() 144 | case "!=": 145 | return newNotEqualExprNode() 146 | } 147 | defer func() { 148 | if e != nil { 149 | *expr = (*expr)[1:] 150 | } 151 | }() 152 | switch a[0] { 153 | // case '&': 154 | // case '|': 155 | // case '^': 156 | case '+': 157 | return newAdditionExprNode() 158 | case '-': 159 | return newSubtractionExprNode() 160 | case '*': 161 | return newMultiplicationExprNode() 162 | case '/': 163 | return newDivisionExprNode() 164 | case '%': 165 | return newRemainderExprNode() 166 | case '<': 167 | return newLessExprNode() 168 | case '>': 169 | return newGreaterExprNode() 170 | } 171 | return nil 172 | } 173 | 174 | // run calculates the value of expression. 175 | func (p *Expr) run(field string, tagExpr *TagExpr) interface{} { 176 | return p.expr.Run(context.Background(), field, tagExpr) 177 | } 178 | 179 | func (p *Expr) runWithEnv(field string, tagExpr *TagExpr, env map[string]interface{}) interface{} { 180 | ctx := context.WithValue(context.Background(), variableKey, env) 181 | return p.expr.Run(ctx, field, tagExpr) 182 | } 183 | 184 | /** 185 | * Priority: 186 | * () ! bool float64 string nil 187 | * * / % 188 | * + - 189 | * < <= > >= 190 | * == != 191 | * && 192 | * || 193 | **/ 194 | 195 | func sortPriority(e ExprNode) { 196 | for subSortPriority(e.RightOperand(), false) { 197 | } 198 | } 199 | 200 | func subSortPriority(e ExprNode, isLeft bool) bool { 201 | if e == nil { 202 | return false 203 | } 204 | leftChanged := subSortPriority(e.LeftOperand(), true) 205 | rightChanged := subSortPriority(e.RightOperand(), false) 206 | if getPriority(e) > getPriority(e.LeftOperand()) { 207 | leftOperandToParent(e, isLeft) 208 | return true 209 | } 210 | return leftChanged || rightChanged 211 | } 212 | 213 | func leftOperandToParent(e ExprNode, isLeft bool) { 214 | le := e.LeftOperand() 215 | if le == nil { 216 | return 217 | } 218 | p := e.Parent() 219 | le.SetParent(p) 220 | if p != nil { 221 | if isLeft { 222 | p.SetLeftOperand(le) 223 | } else { 224 | p.SetRightOperand(le) 225 | } 226 | } 227 | e.SetParent(le) 228 | e.SetLeftOperand(le.RightOperand()) 229 | le.RightOperand().SetParent(e) 230 | le.SetRightOperand(e) 231 | } 232 | 233 | func getPriority(e ExprNode) (i int) { 234 | // defer func() { 235 | // printf("expr:%T %d\n", e, i) 236 | // }() 237 | switch e.(type) { 238 | default: // () ! bool float64 string nil 239 | return 7 240 | case *multiplicationExprNode, *divisionExprNode, *remainderExprNode: // * / % 241 | return 6 242 | case *additionExprNode, *subtractionExprNode: // + - 243 | return 5 244 | case *lessExprNode, *lessEqualExprNode, *greaterExprNode, *greaterEqualExprNode: // < <= > >= 245 | return 4 246 | case *equalExprNode, *notEqualExprNode: // == != 247 | return 3 248 | case *andExprNode: // && 249 | return 2 250 | case *orExprNode: // || 251 | return 1 252 | } 253 | } 254 | 255 | // ExprNode expression interface 256 | type ExprNode interface { 257 | SetParent(ExprNode) 258 | Parent() ExprNode 259 | LeftOperand() ExprNode 260 | RightOperand() ExprNode 261 | SetLeftOperand(ExprNode) 262 | SetRightOperand(ExprNode) 263 | String() string 264 | Run(context.Context, string, *TagExpr) interface{} 265 | } 266 | 267 | // var _ ExprNode = new(exprBackground) 268 | 269 | type exprBackground struct { 270 | parent ExprNode 271 | leftOperand ExprNode 272 | rightOperand ExprNode 273 | } 274 | 275 | func (eb *exprBackground) SetParent(e ExprNode) { 276 | eb.parent = e 277 | } 278 | 279 | func (eb *exprBackground) Parent() ExprNode { 280 | return eb.parent 281 | } 282 | 283 | func (eb *exprBackground) LeftOperand() ExprNode { 284 | return eb.leftOperand 285 | } 286 | 287 | func (eb *exprBackground) RightOperand() ExprNode { 288 | return eb.rightOperand 289 | } 290 | 291 | func (eb *exprBackground) SetLeftOperand(left ExprNode) { 292 | eb.leftOperand = left 293 | } 294 | 295 | func (eb *exprBackground) SetRightOperand(right ExprNode) { 296 | eb.rightOperand = right 297 | } 298 | 299 | func (*exprBackground) Run(context.Context, string, *TagExpr) interface{} { return nil } 300 | 301 | var debugSwitch = goutil.IsGoTest() 302 | 303 | func printf(format string, a ...interface{}) { 304 | if debugSwitch { 305 | fmt.Fprintf(os.Stderr, format, a...) 306 | } 307 | } 308 | 309 | func printExprNode(node ExprNode) { 310 | if node == nil { 311 | return 312 | } 313 | tail := true 314 | if node.Parent() != nil { 315 | tail = node == node.Parent().RightOperand() 316 | } 317 | printf("%s\n\n", formatExprNode(node, 0, tail)) 318 | } 319 | 320 | func formatExprNode(node ExprNode, level int, tail bool) []byte { 321 | var b bytes.Buffer 322 | if node == nil { 323 | } else { 324 | b.Write(formatExprNode(node.LeftOperand(), level+1, false)) 325 | 326 | b.Write(bytes.Repeat([]byte(" "), level)) 327 | if tail { 328 | b.Write([]byte("└── ")) 329 | } else { 330 | b.Write([]byte("┌── ")) 331 | } 332 | 333 | b.Write([]byte(node.String())) 334 | b.Write([]byte("\n")) 335 | 336 | b.Write(formatExprNode(node.RightOperand(), level+1, true)) 337 | } 338 | return b.Bytes() 339 | } 340 | -------------------------------------------------------------------------------- /spec_operator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Bytedance Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tagexpr 16 | 17 | import ( 18 | "context" 19 | "math" 20 | ) 21 | 22 | // --------------------------- Operator --------------------------- 23 | 24 | type additionExprNode struct{ exprBackground } 25 | 26 | func (ae *additionExprNode) String() string { 27 | return "+" 28 | } 29 | 30 | func newAdditionExprNode() ExprNode { return &additionExprNode{} } 31 | 32 | func (ae *additionExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 33 | // positive number or Addition 34 | v0 := ae.leftOperand.Run(ctx, currField, tagExpr) 35 | v1 := ae.rightOperand.Run(ctx, currField, tagExpr) 36 | if s0, ok := toFloat64(v0, false); ok { 37 | s1, _ := toFloat64(v1, true) 38 | return s0 + s1 39 | } 40 | if s0, ok := toString(v0, false); ok { 41 | s1, _ := toString(v1, true) 42 | return s0 + s1 43 | } 44 | return v0 45 | } 46 | 47 | type multiplicationExprNode struct{ exprBackground } 48 | 49 | func (ae *multiplicationExprNode) String() string { 50 | return "*" 51 | } 52 | 53 | func newMultiplicationExprNode() ExprNode { return &multiplicationExprNode{} } 54 | 55 | func (ae *multiplicationExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 56 | v0, _ := toFloat64(ae.leftOperand.Run(ctx, currField, tagExpr), true) 57 | v1, _ := toFloat64(ae.rightOperand.Run(ctx, currField, tagExpr), true) 58 | return v0 * v1 59 | } 60 | 61 | type divisionExprNode struct{ exprBackground } 62 | 63 | func (de *divisionExprNode) String() string { 64 | return "/" 65 | } 66 | 67 | func newDivisionExprNode() ExprNode { return &divisionExprNode{} } 68 | 69 | func (de *divisionExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 70 | v1, _ := toFloat64(de.rightOperand.Run(ctx, currField, tagExpr), true) 71 | if v1 == 0 { 72 | return math.NaN() 73 | } 74 | v0, _ := toFloat64(de.leftOperand.Run(ctx, currField, tagExpr), true) 75 | return v0 / v1 76 | } 77 | 78 | type subtractionExprNode struct{ exprBackground } 79 | 80 | func (de *subtractionExprNode) String() string { 81 | return "-" 82 | } 83 | 84 | func newSubtractionExprNode() ExprNode { return &subtractionExprNode{} } 85 | 86 | func (de *subtractionExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 87 | v0, _ := toFloat64(de.leftOperand.Run(ctx, currField, tagExpr), true) 88 | v1, _ := toFloat64(de.rightOperand.Run(ctx, currField, tagExpr), true) 89 | return v0 - v1 90 | } 91 | 92 | type remainderExprNode struct{ exprBackground } 93 | 94 | func (re *remainderExprNode) String() string { 95 | return "%" 96 | } 97 | 98 | func newRemainderExprNode() ExprNode { return &remainderExprNode{} } 99 | 100 | func (re *remainderExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 101 | v1, _ := toFloat64(re.rightOperand.Run(ctx, currField, tagExpr), true) 102 | if v1 == 0 { 103 | return math.NaN() 104 | } 105 | v0, _ := toFloat64(re.leftOperand.Run(ctx, currField, tagExpr), true) 106 | return float64(int64(v0) % int64(v1)) 107 | } 108 | 109 | type equalExprNode struct{ exprBackground } 110 | 111 | func (ee *equalExprNode) String() string { 112 | return "==" 113 | } 114 | 115 | func newEqualExprNode() ExprNode { return &equalExprNode{} } 116 | 117 | func (ee *equalExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 118 | v0 := ee.leftOperand.Run(ctx, currField, tagExpr) 119 | v1 := ee.rightOperand.Run(ctx, currField, tagExpr) 120 | if v0 == v1 { 121 | return true 122 | } 123 | if s0, ok := toFloat64(v0, false); ok { 124 | if s1, ok := toFloat64(v1, true); ok { 125 | return s0 == s1 126 | } 127 | } 128 | if s0, ok := toString(v0, false); ok { 129 | if s1, ok := toString(v1, true); ok { 130 | return s0 == s1 131 | } 132 | return false 133 | } 134 | switch r := v0.(type) { 135 | case bool: 136 | r1, ok := v1.(bool) 137 | if ok { 138 | return r == r1 139 | } 140 | case nil: 141 | return v1 == nil 142 | } 143 | return false 144 | } 145 | 146 | type notEqualExprNode struct{ equalExprNode } 147 | 148 | func (ne *notEqualExprNode) String() string { 149 | return "!=" 150 | } 151 | 152 | func newNotEqualExprNode() ExprNode { return ¬EqualExprNode{} } 153 | 154 | func (ne *notEqualExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 155 | return !ne.equalExprNode.Run(ctx, currField, tagExpr).(bool) 156 | } 157 | 158 | type greaterExprNode struct{ exprBackground } 159 | 160 | func (ge *greaterExprNode) String() string { 161 | return ">" 162 | } 163 | 164 | func newGreaterExprNode() ExprNode { return &greaterExprNode{} } 165 | 166 | func (ge *greaterExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 167 | v0 := ge.leftOperand.Run(ctx, currField, tagExpr) 168 | v1 := ge.rightOperand.Run(ctx, currField, tagExpr) 169 | if s0, ok := toFloat64(v0, false); ok { 170 | if s1, ok := toFloat64(v1, true); ok { 171 | return s0 > s1 172 | } 173 | } 174 | if s0, ok := toString(v0, false); ok { 175 | if s1, ok := toString(v1, true); ok { 176 | return s0 > s1 177 | } 178 | return false 179 | } 180 | return false 181 | } 182 | 183 | type greaterEqualExprNode struct{ exprBackground } 184 | 185 | func (ge *greaterEqualExprNode) String() string { 186 | return ">=" 187 | } 188 | 189 | func newGreaterEqualExprNode() ExprNode { return &greaterEqualExprNode{} } 190 | 191 | func (ge *greaterEqualExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 192 | v0 := ge.leftOperand.Run(ctx, currField, tagExpr) 193 | v1 := ge.rightOperand.Run(ctx, currField, tagExpr) 194 | if s0, ok := toFloat64(v0, false); ok { 195 | if s1, ok := toFloat64(v1, true); ok { 196 | return s0 >= s1 197 | } 198 | } 199 | if s0, ok := toString(v0, false); ok { 200 | if s1, ok := toString(v1, true); ok { 201 | return s0 >= s1 202 | } 203 | return false 204 | } 205 | return false 206 | } 207 | 208 | type lessExprNode struct{ exprBackground } 209 | 210 | func (le *lessExprNode) String() string { 211 | return "<" 212 | } 213 | 214 | func newLessExprNode() ExprNode { return &lessExprNode{} } 215 | 216 | func (le *lessExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 217 | v0 := le.leftOperand.Run(ctx, currField, tagExpr) 218 | v1 := le.rightOperand.Run(ctx, currField, tagExpr) 219 | if s0, ok := toFloat64(v0, false); ok { 220 | if s1, ok := toFloat64(v1, true); ok { 221 | return s0 < s1 222 | } 223 | } 224 | if s0, ok := toString(v0, false); ok { 225 | if s1, ok := toString(v1, true); ok { 226 | return s0 < s1 227 | } 228 | return false 229 | } 230 | return false 231 | } 232 | 233 | type lessEqualExprNode struct{ exprBackground } 234 | 235 | func (le *lessEqualExprNode) String() string { 236 | return "<=" 237 | } 238 | 239 | func newLessEqualExprNode() ExprNode { return &lessEqualExprNode{} } 240 | 241 | func (le *lessEqualExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 242 | v0 := le.leftOperand.Run(ctx, currField, tagExpr) 243 | v1 := le.rightOperand.Run(ctx, currField, tagExpr) 244 | if s0, ok := toFloat64(v0, false); ok { 245 | if s1, ok := toFloat64(v1, true); ok { 246 | return s0 <= s1 247 | } 248 | } 249 | if s0, ok := toString(v0, false); ok { 250 | if s1, ok := toString(v1, true); ok { 251 | return s0 <= s1 252 | } 253 | return false 254 | } 255 | return false 256 | } 257 | 258 | type andExprNode struct{ exprBackground } 259 | 260 | func (ae *andExprNode) String() string { 261 | return "&&" 262 | } 263 | 264 | func newAndExprNode() ExprNode { return &andExprNode{} } 265 | 266 | func (ae *andExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 267 | for _, e := range [2]ExprNode{ae.leftOperand, ae.rightOperand} { 268 | if !FakeBool(e.Run(ctx, currField, tagExpr)) { 269 | return false 270 | } 271 | } 272 | return true 273 | } 274 | 275 | type orExprNode struct{ exprBackground } 276 | 277 | func (oe *orExprNode) String() string { 278 | return "||" 279 | } 280 | 281 | func newOrExprNode() ExprNode { return &orExprNode{} } 282 | 283 | func (oe *orExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 284 | for _, e := range [2]ExprNode{oe.leftOperand, oe.rightOperand} { 285 | if FakeBool(e.Run(ctx, currField, tagExpr)) { 286 | return true 287 | } 288 | } 289 | return false 290 | } 291 | -------------------------------------------------------------------------------- /binding/gjson/gjson.go: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | 3 | // Copyright (c) 2016 Josh Baker 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | // this software and associated documentation files (the "Software"), to deal in 7 | // the Software without restriction, including without limitation the rights to 8 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | // the Software, and to permit persons to whom the Software is furnished to do so, 10 | // subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package gjson 23 | 24 | import ( 25 | "encoding/base64" 26 | "encoding/json" 27 | "errors" 28 | "fmt" 29 | "reflect" 30 | "strings" 31 | 32 | "github.com/andeya/ameda" 33 | "github.com/andeya/goutil" 34 | "github.com/bytedance/go-tagexpr/v2/binding" 35 | "github.com/bytedance/go-tagexpr/v2/binding/gjson/internal/caching" 36 | "github.com/bytedance/go-tagexpr/v2/binding/gjson/internal/rt" 37 | gjson "github.com/bytedance/go-tagexpr/v2/binding/tidwall_gjson" 38 | ) 39 | 40 | var ( 41 | programCache = caching.CreateProgramCache() 42 | unmarshalerInterface = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() 43 | ) 44 | 45 | func init() { 46 | gjson.DisableModifiers = true 47 | } 48 | 49 | // UseJSONUnmarshaler reset the JSON Unmarshaler of binding. 50 | func UseJSONUnmarshaler() { 51 | binding.Default().ResetJSONUnmarshaler(Unmarshal) 52 | } 53 | 54 | // Unmarshal Unmarshal JSON, old version compatible. 55 | func Unmarshal(data []byte, v interface{}) error { 56 | val, ok := v.(reflect.Value) 57 | if !ok { 58 | val = reflect.ValueOf(v) 59 | } 60 | return assign(gjson.ParseBytes(data), val) 61 | } 62 | 63 | // assign Unmarshal 64 | func assign(jsval gjson.Result, goval reflect.Value) (err error) { 65 | if jsval.Type == gjson.Null { 66 | return nil 67 | } 68 | t := goval.Type() 69 | switch goval.Kind() { 70 | default: 71 | case reflect.Ptr: 72 | if !ameda.InitPointer(goval) { 73 | return errors.New("v cannot be set") 74 | } 75 | newval := ameda.DereferencePtrValue(goval) 76 | if err = assign(jsval, newval); err != nil { 77 | return err 78 | } 79 | case reflect.Struct: 80 | sf := getFiledInfo(t) 81 | jsval.ForEach(func(key, value gjson.Result) bool { 82 | if idx, ok := sf[key.Str]; ok { 83 | f := fieldByIndex(goval, idx) 84 | if f.CanSet() { 85 | if err = assign(value, f); err != nil { 86 | return false 87 | } 88 | } 89 | } 90 | return true 91 | }) 92 | case reflect.Slice: 93 | if t.Elem().Kind() == reflect.Uint8 && jsval.Type == gjson.String { 94 | var data []byte 95 | data, err = base64.StdEncoding.DecodeString(jsval.String()) 96 | if err != nil { 97 | return err 98 | } 99 | goval.Set(reflect.ValueOf(data)) 100 | } else { 101 | if !jsval.IsArray() { 102 | // canAddr: true, implement unmarshaler : true -> continue 103 | // canAddr: true, implement unmarshaler : false -> return 104 | // canAddr: false, implement unmarshaler : true -> return 105 | // canAddr: false, implement unmarshaler : false -> return 106 | if !goval.CanAddr() || !goval.Addr().Type().Implements(unmarshalerInterface) { 107 | return nil 108 | } 109 | } 110 | jsvals := jsval.Array() 111 | slice := reflect.MakeSlice(t, len(jsvals), len(jsvals)) 112 | for i := 0; i < len(jsvals); i++ { 113 | if err = assign(jsvals[i], slice.Index(i)); err != nil { 114 | return err 115 | } 116 | } 117 | goval.Set(slice) 118 | } 119 | case reflect.Array: 120 | i, n := 0, goval.Len() 121 | jsval.ForEach(func(_, value gjson.Result) bool { 122 | if i == n { 123 | return false 124 | } 125 | if err = assign(value, goval.Index(i)); err != nil { 126 | return false 127 | } 128 | i++ 129 | return true 130 | }) 131 | case reflect.Map: 132 | if jsval.Type == gjson.JSON && t.Key().Kind() == reflect.String { 133 | if t.Elem().Kind() == reflect.Interface { 134 | goval.Set(reflect.ValueOf(jsval.Value())) 135 | } else { 136 | if goval.IsNil() { 137 | goval.Set(reflect.MakeMap(t)) 138 | } 139 | valType := t.Elem() 140 | keyType := goval.Type().Key() 141 | switch keyType.Kind() { 142 | case reflect.String, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 143 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 144 | default: 145 | return fmt.Errorf("gjson: unsupported type: %s", keyType) 146 | } 147 | jsval.ForEach(func(key, value gjson.Result) bool { 148 | val := reflect.New(valType) 149 | if err = assign(value, val); err != nil { 150 | return false 151 | } 152 | goval.SetMapIndex(reflect.ValueOf(key.String()).Convert(keyType), val.Elem()) 153 | return true 154 | }) 155 | } 156 | } 157 | case reflect.Interface: 158 | goval.Set(reflect.ValueOf(jsval.Value())) 159 | case reflect.Bool: 160 | goval.SetBool(jsval.Bool()) 161 | case reflect.Float32, reflect.Float64: 162 | goval.SetFloat(jsval.Float()) 163 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 164 | goval.SetInt(jsval.Int()) 165 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 166 | goval.SetUint(jsval.Uint()) 167 | case reflect.String: 168 | goval.SetString(jsval.String()) 169 | } 170 | if len(t.PkgPath()) > 0 { 171 | v := goval.Addr() 172 | if v.Type().NumMethod() > 0 { 173 | if u, ok := v.Interface().(json.Unmarshaler); ok { 174 | if err = u.UnmarshalJSON([]byte(jsval.Raw)); err != nil { 175 | return err 176 | } 177 | } 178 | } 179 | } 180 | return err 181 | } 182 | 183 | func getFiledInfo(t reflect.Type) map[string][]int { 184 | vtr := rt.UnpackType(t) 185 | filedInfo := programCache.Get(vtr) 186 | if filedInfo == nil { 187 | pp, err := programCache.Compute(vtr, computeTypeInfo) 188 | if err == nil { 189 | return pp.(map[string][]int) 190 | } 191 | filedInfo, _ = computeTypeInfo(vtr) 192 | } 193 | return filedInfo.(map[string][]int) 194 | } 195 | 196 | func computeTypeInfo(vtr *rt.GoType) (interface{}, error) { 197 | t := vtr.Pack() 198 | sf := make(map[string][]int) 199 | numField := t.NumField() 200 | for i := 0; i < numField; i++ { 201 | f := t.Field(i) 202 | if !f.Anonymous && !goutil.IsExportedName(f.Name) { 203 | continue 204 | } 205 | tag := getJsonTag(f.Tag) 206 | if tag == "-" { 207 | continue 208 | } 209 | if tag != "" { 210 | sf[tag] = []int{i} 211 | } else if f.Anonymous { 212 | if findAnonymous(ameda.DereferenceType(f.Type), []int{i}, sf, 20) { 213 | continue 214 | } 215 | } 216 | if tag != f.Name { 217 | sf[f.Name] = []int{i} 218 | } 219 | } 220 | return sf, nil 221 | } 222 | 223 | func getJsonTag(tag reflect.StructTag) string { 224 | return strings.Split(tag.Get("json"), ",")[0] 225 | } 226 | 227 | func findAnonymous(t reflect.Type, i []int, sf map[string][]int, depth int) bool { 228 | depth-- 229 | if depth < 0 { 230 | return true 231 | } 232 | if t.Kind() == reflect.Struct { 233 | subNumField := t.NumField() 234 | for ii := 0; ii < subNumField; ii++ { 235 | ff := t.Field(ii) 236 | subTag := getJsonTag(ff.Tag) 237 | if subTag == "-" { 238 | continue 239 | } 240 | a := append(i, ii) 241 | if subTag != "" { 242 | sf[subTag] = a 243 | } else if ff.Anonymous { 244 | tt := ameda.DereferenceType(ff.Type) 245 | if tt.String() == t.String() { 246 | continue 247 | } 248 | if findAnonymous(tt, a, sf, depth) { 249 | continue 250 | } 251 | } 252 | if subTag != ff.Name { 253 | sf[ff.Name] = a 254 | } 255 | } 256 | return true 257 | } 258 | return false 259 | } 260 | 261 | func fieldByIndex(v reflect.Value, index []int) reflect.Value { 262 | if len(index) == 1 { 263 | return v.Field(index[0]) 264 | } 265 | if v.Kind() != reflect.Struct { 266 | return reflect.Value{} 267 | } 268 | v = v.Field(index[0]) 269 | for _, x := range index[1:] { 270 | for v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct { 271 | if v.IsNil() { 272 | if v.CanSet() { 273 | ptrDepth := 0 274 | t := v.Type() 275 | for t.Kind() == reflect.Ptr { 276 | t = t.Elem() 277 | ptrDepth++ 278 | } 279 | v.Set(ameda.ReferenceValue(reflect.New(t), ptrDepth-1)) 280 | v = ameda.DereferencePtrValue(v) 281 | } else { 282 | return reflect.Value{} 283 | } 284 | } else { 285 | v = ameda.DereferencePtrValue(v) 286 | } 287 | } 288 | v = v.Field(x) 289 | } 290 | return v 291 | } 292 | -------------------------------------------------------------------------------- /spec_func.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Bytedance Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tagexpr 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "reflect" 21 | "regexp" 22 | "strings" 23 | 24 | "github.com/andeya/goutil/errors" 25 | ) 26 | 27 | // --------------------------- Custom function --------------------------- 28 | 29 | var funcList = map[string]func(p *Expr, expr *string) ExprNode{} 30 | 31 | // MustRegFunc registers function expression. 32 | // NOTE: 33 | // 34 | // example: len($), regexp("\\d") or regexp("\\d",$); 35 | // If @force=true, allow to cover the existed same @funcName; 36 | // The go number types always are float64; 37 | // The go string types always are string; 38 | // Panic if there is an error. 39 | func MustRegFunc(funcName string, fn func(...interface{}) interface{}, force ...bool) { 40 | err := RegFunc(funcName, fn, force...) 41 | if err != nil { 42 | panic(err) 43 | } 44 | } 45 | 46 | // RegFunc registers function expression. 47 | // NOTE: 48 | // 49 | // example: len($), regexp("\\d") or regexp("\\d",$); 50 | // If @force=true, allow to cover the existed same @funcName; 51 | // The go number types always are float64; 52 | // The go string types always are string. 53 | func RegFunc(funcName string, fn func(...interface{}) interface{}, force ...bool) error { 54 | if len(force) == 0 || !force[0] { 55 | _, ok := funcList[funcName] 56 | if ok { 57 | return errors.Errorf("duplicate registration expression function: %s", funcName) 58 | } 59 | } 60 | funcList[funcName] = newFunc(funcName, fn) 61 | return nil 62 | } 63 | 64 | func (p *Expr) parseFuncSign(funcName string, expr *string) (boolOpposite *bool, signOpposite *bool, args []ExprNode, found bool) { 65 | prefix := funcName + "(" 66 | length := len(funcName) 67 | last, boolOpposite, signOpposite := getBoolAndSignOpposite(expr) 68 | if !strings.HasPrefix(last, prefix) { 69 | return 70 | } 71 | *expr = last[length:] 72 | lastStr := *expr 73 | subExprNode := readPairedSymbol(expr, '(', ')') 74 | if subExprNode == nil { 75 | return 76 | } 77 | *subExprNode = "," + *subExprNode 78 | for { 79 | if strings.HasPrefix(*subExprNode, ",") { 80 | *subExprNode = (*subExprNode)[1:] 81 | operand := newGroupExprNode() 82 | err := p.parseExprNode(trimLeftSpace(subExprNode), operand) 83 | if err != nil { 84 | *expr = lastStr 85 | return 86 | } 87 | sortPriority(operand) 88 | args = append(args, operand) 89 | } else { 90 | *expr = lastStr 91 | return 92 | } 93 | trimLeftSpace(subExprNode) 94 | if len(*subExprNode) == 0 { 95 | found = true 96 | return 97 | } 98 | } 99 | } 100 | 101 | func newFunc(funcName string, fn func(...interface{}) interface{}) func(*Expr, *string) ExprNode { 102 | return func(p *Expr, expr *string) ExprNode { 103 | boolOpposite, signOpposite, args, found := p.parseFuncSign(funcName, expr) 104 | if !found { 105 | return nil 106 | } 107 | return &funcExprNode{ 108 | fn: fn, 109 | boolOpposite: boolOpposite, 110 | signOpposite: signOpposite, 111 | args: args, 112 | } 113 | } 114 | } 115 | 116 | type funcExprNode struct { 117 | exprBackground 118 | args []ExprNode 119 | fn func(...interface{}) interface{} 120 | boolOpposite *bool 121 | signOpposite *bool 122 | } 123 | 124 | func (f *funcExprNode) String() string { 125 | return "func()" 126 | } 127 | 128 | func (f *funcExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 129 | var args []interface{} 130 | if n := len(f.args); n > 0 { 131 | args = make([]interface{}, n) 132 | for k, v := range f.args { 133 | args[k] = v.Run(ctx, currField, tagExpr) 134 | } 135 | } 136 | return realValue(f.fn(args...), f.boolOpposite, f.signOpposite) 137 | } 138 | 139 | // --------------------------- Built-in function --------------------------- 140 | func init() { 141 | funcList["regexp"] = readRegexpFuncExprNode 142 | funcList["sprintf"] = readSprintfFuncExprNode 143 | funcList["range"] = readRangeFuncExprNode 144 | // len: Built-in function len, the length of struct field X 145 | MustRegFunc("len", func(args ...interface{}) (n interface{}) { 146 | if len(args) != 1 { 147 | return 0 148 | } 149 | v := args[0] 150 | switch e := v.(type) { 151 | case string: 152 | return float64(len(e)) 153 | case float64, bool, nil: 154 | return 0 155 | } 156 | defer func() { 157 | if recover() != nil { 158 | n = 0 159 | } 160 | }() 161 | return float64(reflect.ValueOf(v).Len()) 162 | }, true) 163 | // mblen: get the length of string field X (character number) 164 | MustRegFunc("mblen", func(args ...interface{}) (n interface{}) { 165 | if len(args) != 1 { 166 | return 0 167 | } 168 | v := args[0] 169 | switch e := v.(type) { 170 | case string: 171 | return float64(len([]rune(e))) 172 | case float64, bool, nil: 173 | return 0 174 | } 175 | defer func() { 176 | if recover() != nil { 177 | n = 0 178 | } 179 | }() 180 | return float64(reflect.ValueOf(v).Len()) 181 | }, true) 182 | 183 | // in: Check if the first parameter is one of the enumerated parameters 184 | MustRegFunc("in", func(args ...interface{}) interface{} { 185 | switch len(args) { 186 | case 0: 187 | return true 188 | case 1: 189 | return false 190 | default: 191 | elem := args[0] 192 | set := args[1:] 193 | for _, e := range set { 194 | if elem == e { 195 | return true 196 | } 197 | } 198 | return false 199 | } 200 | }, true) 201 | } 202 | 203 | type regexpFuncExprNode struct { 204 | exprBackground 205 | re *regexp.Regexp 206 | boolOpposite bool 207 | } 208 | 209 | func (re *regexpFuncExprNode) String() string { 210 | return "regexp()" 211 | } 212 | 213 | func readRegexpFuncExprNode(p *Expr, expr *string) ExprNode { 214 | last, boolOpposite, _ := getBoolAndSignOpposite(expr) 215 | if !strings.HasPrefix(last, "regexp(") { 216 | return nil 217 | } 218 | *expr = last[6:] 219 | lastStr := *expr 220 | subExprNode := readPairedSymbol(expr, '(', ')') 221 | if subExprNode == nil { 222 | return nil 223 | } 224 | s := readPairedSymbol(trimLeftSpace(subExprNode), '\'', '\'') 225 | if s == nil { 226 | *expr = lastStr 227 | return nil 228 | } 229 | rege, err := regexp.Compile(*s) 230 | if err != nil { 231 | *expr = lastStr 232 | return nil 233 | } 234 | operand := newGroupExprNode() 235 | trimLeftSpace(subExprNode) 236 | if strings.HasPrefix(*subExprNode, ",") { 237 | *subExprNode = (*subExprNode)[1:] 238 | err = p.parseExprNode(trimLeftSpace(subExprNode), operand) 239 | if err != nil { 240 | *expr = lastStr 241 | return nil 242 | } 243 | } else { 244 | var currFieldVal = "$" 245 | p.parseExprNode(&currFieldVal, operand) 246 | } 247 | trimLeftSpace(subExprNode) 248 | if *subExprNode != "" { 249 | *expr = lastStr 250 | return nil 251 | } 252 | e := ®expFuncExprNode{ 253 | re: rege, 254 | } 255 | if boolOpposite != nil { 256 | e.boolOpposite = *boolOpposite 257 | } 258 | e.SetRightOperand(operand) 259 | return e 260 | } 261 | 262 | func (re *regexpFuncExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 263 | param := re.rightOperand.Run(ctx, currField, tagExpr) 264 | switch v := param.(type) { 265 | case string: 266 | bol := re.re.MatchString(v) 267 | if re.boolOpposite { 268 | return !bol 269 | } 270 | return bol 271 | case float64, bool: 272 | return false 273 | } 274 | v := reflect.ValueOf(param) 275 | if v.Kind() == reflect.String { 276 | bol := re.re.MatchString(v.String()) 277 | if re.boolOpposite { 278 | return !bol 279 | } 280 | return bol 281 | } 282 | return false 283 | } 284 | 285 | type sprintfFuncExprNode struct { 286 | exprBackground 287 | format string 288 | args []ExprNode 289 | } 290 | 291 | func (se *sprintfFuncExprNode) String() string { 292 | return "sprintf()" 293 | } 294 | 295 | func readSprintfFuncExprNode(p *Expr, expr *string) ExprNode { 296 | if !strings.HasPrefix(*expr, "sprintf(") { 297 | return nil 298 | } 299 | *expr = (*expr)[7:] 300 | lastStr := *expr 301 | subExprNode := readPairedSymbol(expr, '(', ')') 302 | if subExprNode == nil { 303 | return nil 304 | } 305 | format := readPairedSymbol(trimLeftSpace(subExprNode), '\'', '\'') 306 | if format == nil { 307 | *expr = lastStr 308 | return nil 309 | } 310 | e := &sprintfFuncExprNode{ 311 | format: *format, 312 | } 313 | for { 314 | trimLeftSpace(subExprNode) 315 | if len(*subExprNode) == 0 { 316 | return e 317 | } 318 | if strings.HasPrefix(*subExprNode, ",") { 319 | *subExprNode = (*subExprNode)[1:] 320 | operand := newGroupExprNode() 321 | err := p.parseExprNode(trimLeftSpace(subExprNode), operand) 322 | if err != nil { 323 | *expr = lastStr 324 | return nil 325 | } 326 | sortPriority(operand) 327 | e.args = append(e.args, operand) 328 | } else { 329 | *expr = lastStr 330 | return nil 331 | } 332 | } 333 | } 334 | 335 | func (se *sprintfFuncExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 336 | var args []interface{} 337 | if n := len(se.args); n > 0 { 338 | args = make([]interface{}, n) 339 | for i, e := range se.args { 340 | args[i] = e.Run(ctx, currField, tagExpr) 341 | } 342 | } 343 | return fmt.Sprintf(se.format, args...) 344 | } 345 | -------------------------------------------------------------------------------- /spec_operand.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Bytedance Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tagexpr 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "reflect" 21 | "regexp" 22 | "strconv" 23 | "strings" 24 | 25 | "github.com/andeya/ameda" 26 | ) 27 | 28 | // --------------------------- Operand --------------------------- 29 | 30 | type groupExprNode struct { 31 | exprBackground 32 | boolOpposite *bool 33 | signOpposite *bool 34 | } 35 | 36 | func newGroupExprNode() ExprNode { return &groupExprNode{} } 37 | 38 | func readGroupExprNode(expr *string) (grp ExprNode, subExprNode *string) { 39 | last, boolOpposite, signOpposite := getBoolAndSignOpposite(expr) 40 | sptr := readPairedSymbol(&last, '(', ')') 41 | if sptr == nil { 42 | return nil, nil 43 | } 44 | *expr = last 45 | e := &groupExprNode{boolOpposite: boolOpposite, signOpposite: signOpposite} 46 | return e, sptr 47 | } 48 | 49 | func (ge *groupExprNode) String() string { 50 | return "()" 51 | } 52 | 53 | func (ge *groupExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 54 | if ge.rightOperand == nil { 55 | return nil 56 | } 57 | return realValue(ge.rightOperand.Run(ctx, currField, tagExpr), ge.boolOpposite, ge.signOpposite) 58 | } 59 | 60 | type boolExprNode struct { 61 | exprBackground 62 | val bool 63 | } 64 | 65 | func (be *boolExprNode) String() string { 66 | return fmt.Sprintf("%v", be.val) 67 | } 68 | 69 | var boolRegexp = regexp.MustCompile(`^!*(true|false)([\)\],\|&!= \t]{1}|$)`) 70 | 71 | func readBoolExprNode(expr *string) ExprNode { 72 | s := boolRegexp.FindString(*expr) 73 | if s == "" { 74 | return nil 75 | } 76 | last := s[len(s)-1] 77 | if last != 'e' { 78 | s = s[:len(s)-1] 79 | } 80 | *expr = (*expr)[len(s):] 81 | e := &boolExprNode{} 82 | if strings.Contains(s, "t") { 83 | e.val = (len(s)-4)&1 == 0 84 | } else { 85 | e.val = (len(s)-5)&1 == 1 86 | } 87 | return e 88 | } 89 | 90 | func (be *boolExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 91 | return be.val 92 | } 93 | 94 | type stringExprNode struct { 95 | exprBackground 96 | val interface{} 97 | } 98 | 99 | func (se *stringExprNode) String() string { 100 | return fmt.Sprintf("%v", se.val) 101 | } 102 | 103 | func readStringExprNode(expr *string) ExprNode { 104 | last, boolOpposite, _ := getBoolAndSignOpposite(expr) 105 | sptr := readPairedSymbol(&last, '\'', '\'') 106 | if sptr == nil { 107 | return nil 108 | } 109 | *expr = last 110 | e := &stringExprNode{val: realValue(*sptr, boolOpposite, nil)} 111 | return e 112 | } 113 | 114 | func (se *stringExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 115 | return se.val 116 | } 117 | 118 | type digitalExprNode struct { 119 | exprBackground 120 | val interface{} 121 | } 122 | 123 | func (de *digitalExprNode) String() string { 124 | return fmt.Sprintf("%v", de.val) 125 | } 126 | 127 | var digitalRegexp = regexp.MustCompile(`^[\+\-]?\d+(\.\d+)?([\)\],\+\-\*\/%><\|&!=\^ \t\\]|$)`) 128 | 129 | func readDigitalExprNode(expr *string) ExprNode { 130 | last, boolOpposite := getOpposite(expr, "!") 131 | s := digitalRegexp.FindString(last) 132 | if s == "" { 133 | return nil 134 | } 135 | if r := s[len(s)-1]; r < '0' || r > '9' { 136 | s = s[:len(s)-1] 137 | } 138 | *expr = last[len(s):] 139 | f64, _ := strconv.ParseFloat(s, 64) 140 | return &digitalExprNode{val: realValue(f64, boolOpposite, nil)} 141 | } 142 | 143 | func (de *digitalExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 144 | return de.val 145 | } 146 | 147 | type nilExprNode struct { 148 | exprBackground 149 | val interface{} 150 | } 151 | 152 | func (ne *nilExprNode) String() string { 153 | return "" 154 | } 155 | 156 | var nilRegexp = regexp.MustCompile(`^nil([\)\],\|&!= \t]{1}|$)`) 157 | 158 | func readNilExprNode(expr *string) ExprNode { 159 | last, boolOpposite := getOpposite(expr, "!") 160 | s := nilRegexp.FindString(last) 161 | if s == "" { 162 | return nil 163 | } 164 | *expr = last[3:] 165 | return &nilExprNode{val: realValue(nil, boolOpposite, nil)} 166 | } 167 | 168 | func (ne *nilExprNode) Run(ctx context.Context, currField string, tagExpr *TagExpr) interface{} { 169 | return ne.val 170 | } 171 | 172 | type variableExprNode struct { 173 | exprBackground 174 | boolOpposite *bool 175 | val string 176 | } 177 | 178 | func (ve *variableExprNode) String() string { 179 | return fmt.Sprintf("%v", ve.val) 180 | } 181 | 182 | func (ve *variableExprNode) Run(ctx context.Context, variableName string, _ *TagExpr) interface{} { 183 | envObj := ctx.Value(variableKey) 184 | if envObj == nil { 185 | return nil 186 | } 187 | 188 | env := envObj.(map[string]interface{}) 189 | if len(env) == 0 { 190 | return nil 191 | } 192 | 193 | if value, ok := env[ve.val]; ok && value != nil { 194 | return realValue(value, ve.boolOpposite, nil) 195 | } else { 196 | return nil 197 | } 198 | } 199 | 200 | var variableRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*`) 201 | 202 | func readVariableExprNode(expr *string) ExprNode { 203 | last, boolOpposite := getOpposite(expr, "!") 204 | variable := variableRegex.FindString(last) 205 | if variable == "" { 206 | return nil 207 | } 208 | 209 | *expr = (*expr)[len(*expr)-len(last)+len(variable):] 210 | 211 | return &variableExprNode{ 212 | val: variable, 213 | boolOpposite: boolOpposite, 214 | } 215 | } 216 | 217 | 218 | func getBoolAndSignOpposite(expr *string) (last string, boolOpposite *bool, signOpposite *bool) { 219 | last = strings.TrimLeft(last, "+") 220 | last, boolOpposite = getOpposite(expr, "!") 221 | last = strings.TrimLeft(last, "+") 222 | last, signOpposite = getOpposite(&last, "-") 223 | last = strings.TrimLeft(last, "+") 224 | return 225 | } 226 | 227 | func getOpposite(expr *string, cutset string) (string, *bool) { 228 | last := strings.TrimLeft(*expr, cutset) 229 | n := len(*expr) - len(last) 230 | if n == 0 { 231 | return last, nil 232 | } 233 | bol := n&1 == 1 234 | return last, &bol 235 | } 236 | 237 | func toString(i interface{}, enforce bool) (string, bool) { 238 | switch vv := i.(type) { 239 | case string: 240 | return vv, true 241 | case nil: 242 | return "", false 243 | default: 244 | rv := ameda.DereferenceValue(reflect.ValueOf(i)) 245 | if rv.Kind() == reflect.String { 246 | return rv.String(), true 247 | } 248 | if enforce { 249 | if rv.IsValid() && rv.CanInterface() { 250 | return fmt.Sprint(rv.Interface()), true 251 | } else { 252 | return fmt.Sprint(i), true 253 | } 254 | } 255 | } 256 | return "", false 257 | } 258 | 259 | func toFloat64(i interface{}, tryParse bool) (float64, bool) { 260 | var v float64 261 | var ok = true 262 | switch t := i.(type) { 263 | case float64: 264 | v = t 265 | case float32: 266 | v = float64(t) 267 | case int: 268 | v = float64(t) 269 | case int8: 270 | v = float64(t) 271 | case int16: 272 | v = float64(t) 273 | case int32: 274 | v = float64(t) 275 | case int64: 276 | v = float64(t) 277 | case uint: 278 | v = float64(t) 279 | case uint8: 280 | v = float64(t) 281 | case uint16: 282 | v = float64(t) 283 | case uint32: 284 | v = float64(t) 285 | case uint64: 286 | v = float64(t) 287 | case nil: 288 | ok = false 289 | default: 290 | rv := ameda.DereferenceValue(reflect.ValueOf(t)) 291 | switch rv.Kind() { 292 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 293 | v = float64(rv.Int()) 294 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 295 | v = float64(rv.Uint()) 296 | case reflect.Float32, reflect.Float64: 297 | v = rv.Float() 298 | default: 299 | if tryParse { 300 | if s, ok := toString(i, false); ok { 301 | var err error 302 | v, err = strconv.ParseFloat(s, 64) 303 | return v, err == nil 304 | } 305 | } 306 | ok = false 307 | } 308 | } 309 | return v, ok 310 | } 311 | 312 | func realValue(v interface{}, boolOpposite *bool, signOpposite *bool) interface{} { 313 | if boolOpposite != nil { 314 | bol := FakeBool(v) 315 | if *boolOpposite { 316 | return !bol 317 | } 318 | return bol 319 | } 320 | switch t := v.(type) { 321 | case float64, string: 322 | case float32: 323 | v = float64(t) 324 | case int: 325 | v = float64(t) 326 | case int8: 327 | v = float64(t) 328 | case int16: 329 | v = float64(t) 330 | case int32: 331 | v = float64(t) 332 | case int64: 333 | v = float64(t) 334 | case uint: 335 | v = float64(t) 336 | case uint8: 337 | v = float64(t) 338 | case uint16: 339 | v = float64(t) 340 | case uint32: 341 | v = float64(t) 342 | case uint64: 343 | v = float64(t) 344 | case []interface{}: 345 | for k, v := range t { 346 | t[k] = realValue(v, boolOpposite, signOpposite) 347 | } 348 | default: 349 | rv := ameda.DereferenceValue(reflect.ValueOf(v)) 350 | switch rv.Kind() { 351 | case reflect.String: 352 | v = rv.String() 353 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 354 | v = float64(rv.Int()) 355 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 356 | v = float64(rv.Uint()) 357 | case reflect.Float32, reflect.Float64: 358 | v = rv.Float() 359 | } 360 | } 361 | if signOpposite != nil && *signOpposite { 362 | if f, ok := v.(float64); ok { 363 | v = -f 364 | } 365 | } 366 | return v 367 | } 368 | -------------------------------------------------------------------------------- /validator/validator_test.go: -------------------------------------------------------------------------------- 1 | package validator_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | vd "github.com/bytedance/go-tagexpr/v2/validator" 11 | ) 12 | 13 | func TestNil(t *testing.T) { 14 | type F struct { 15 | f struct { 16 | g int `vd:"$%3==1"` 17 | } 18 | } 19 | assert.EqualError(t, vd.Validate((*F)(nil)), "unsupport data: nil") 20 | } 21 | 22 | func TestAll(t *testing.T) { 23 | type T struct { 24 | a string `vd:"email($)"` 25 | f struct { 26 | g int `vd:"$%3==1"` 27 | } 28 | } 29 | assert.EqualError(t, vd.Validate(new(T), true), "email format is incorrect\tinvalid parameter: f.g") 30 | } 31 | 32 | func TestIssue1(t *testing.T) { 33 | type MailBox struct { 34 | Address *string `vd:"email($)"` 35 | Name *string 36 | } 37 | type EmailMsg struct { 38 | Recipients []*MailBox 39 | RecipientsCc []*MailBox 40 | RecipientsBcc []*MailBox 41 | Subject *string 42 | Content *string 43 | AttachmentIDList []string 44 | ReplyTo *string 45 | Params map[string]string 46 | FromEmailAddress *string 47 | FromEmailName *string 48 | } 49 | type EmailTaskInfo struct { 50 | Msg *EmailMsg 51 | StartTimeMS *int64 52 | LogTag *string 53 | } 54 | type BatchCreateEmailTaskRequest struct { 55 | InfoList []*EmailTaskInfo 56 | } 57 | var invalid = "invalid email" 58 | req := &BatchCreateEmailTaskRequest{ 59 | InfoList: []*EmailTaskInfo{ 60 | { 61 | Msg: &EmailMsg{ 62 | Recipients: []*MailBox{ 63 | { 64 | Address: &invalid, 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | } 71 | assert.EqualError(t, vd.Validate(req, false), "email format is incorrect") 72 | } 73 | 74 | func TestIssue2(t *testing.T) { 75 | type a struct { 76 | m map[string]interface{} 77 | } 78 | A := &a{ 79 | m: map[string]interface{}{ 80 | "1": 1, 81 | "2": nil, 82 | }, 83 | } 84 | v := vd.New("vd") 85 | assert.NoError(t, v.Validate(A)) 86 | } 87 | 88 | func TestIssue3(t *testing.T) { 89 | type C struct { 90 | Id string 91 | Index int32 `vd:"$==1"` 92 | } 93 | type A struct { 94 | F1 *C 95 | F2 *C 96 | } 97 | a := &A{ 98 | F1: &C{ 99 | Id: "test", 100 | Index: 1, 101 | }, 102 | } 103 | v := vd.New("vd") 104 | assert.NoError(t, v.Validate(a)) 105 | } 106 | 107 | func TestIssue4(t *testing.T) { 108 | type C struct { 109 | Index *int32 `vd:"@:$!=nil;msg:'index is nil'"` 110 | Index2 *int32 `vd:"$!=nil"` 111 | Index3 *int32 `vd:"$!=nil"` 112 | } 113 | type A struct { 114 | F1 *C 115 | F2 map[string]*C 116 | F3 []*C 117 | } 118 | v := vd.New("vd") 119 | 120 | a := &A{} 121 | assert.NoError(t, v.Validate(a)) 122 | 123 | a = &A{F1: new(C)} 124 | assert.EqualError(t, v.Validate(a), "index is nil") 125 | 126 | a = &A{F2: map[string]*C{"x": &C{Index: new(int32)}}} 127 | assert.EqualError(t, v.Validate(a), "invalid parameter: F2{v for k=x}.Index2") 128 | 129 | a = &A{F3: []*C{{Index: new(int32)}}} 130 | assert.EqualError(t, v.Validate(a), "invalid parameter: F3[0].Index2") 131 | 132 | type B struct { 133 | F1 *C `vd:"$!=nil"` 134 | F2 *C 135 | } 136 | b := &B{} 137 | assert.EqualError(t, v.Validate(b), "invalid parameter: F1") 138 | 139 | type D struct { 140 | F1 *C 141 | F2 *C 142 | } 143 | 144 | type E struct { 145 | D []*D 146 | } 147 | b.F1 = new(C) 148 | e := &E{D: []*D{nil}} 149 | assert.NoError(t, v.Validate(e)) 150 | } 151 | 152 | func TestIssue5(t *testing.T) { 153 | type SubSheet struct { 154 | } 155 | type CopySheet struct { 156 | Source *SubSheet `json:"source" vd:"$!=nil"` 157 | Destination *SubSheet `json:"destination" vd:"$!=nil"` 158 | } 159 | type UpdateSheetsRequest struct { 160 | CopySheet *CopySheet `json:"copySheet"` 161 | } 162 | type BatchUpdateSheetRequestArg struct { 163 | Requests []*UpdateSheetsRequest `json:"requests"` 164 | } 165 | b := `{"requests": [{}]}` 166 | var data BatchUpdateSheetRequestArg 167 | err := json.Unmarshal([]byte(b), &data) 168 | assert.NoError(t, err) 169 | assert.Equal(t, 1, len(data.Requests)) 170 | assert.Nil(t, data.Requests[0].CopySheet) 171 | v := vd.New("vd") 172 | assert.NoError(t, v.Validate(&data)) 173 | } 174 | 175 | func TestIn(t *testing.T) { 176 | type S string 177 | type I int16 178 | type T struct { 179 | X *int `vd:"$==nil || len($)>0"` 180 | A S `vd:"in($,'a','b','c')"` 181 | B I `vd:"in($,1,2.0,3)"` 182 | } 183 | v := vd.New("vd") 184 | data := &T{} 185 | err := v.Validate(data) 186 | assert.EqualError(t, err, "invalid parameter: A") 187 | data.A = "b" 188 | err = v.Validate(data) 189 | assert.EqualError(t, err, "invalid parameter: B") 190 | data.B = 2 191 | err = v.Validate(data) 192 | assert.NoError(t, err) 193 | 194 | type T2 struct { 195 | C string `vd:"in($)"` 196 | } 197 | data2 := &T2{} 198 | err = v.Validate(data2) 199 | assert.EqualError(t, err, "invalid parameter: C") 200 | 201 | type T3 struct { 202 | C string `vd:"in($,1)"` 203 | } 204 | data3 := &T3{} 205 | err = v.Validate(data3) 206 | assert.EqualError(t, err, "invalid parameter: C") 207 | } 208 | 209 | type ( 210 | Issue23A struct { 211 | b *Issue23B 212 | v int64 `vd:"$==0"` 213 | } 214 | Issue23B struct { 215 | a *Issue23A 216 | v int64 `vd:"$==0"` 217 | } 218 | ) 219 | 220 | func TestIssue23(t *testing.T) { 221 | var data = &Issue23B{a: &Issue23A{b: new(Issue23B)}} 222 | err := vd.Validate(data, true) 223 | assert.NoError(t, err) 224 | } 225 | 226 | func TestIssue24(t *testing.T) { 227 | type SubmitDoctorImportItem struct { 228 | Name string `form:"name,required" json:"name,required" query:"name,required"` 229 | Avatar *string `form:"avatar,omitempty" json:"avatar,omitempty" query:"avatar,omitempty"` 230 | Idcard string `form:"idcard,required" json:"idcard,required" query:"idcard,required" vd:"len($)==18"` 231 | IdcardPics []string `form:"idcard_pics,omitempty" json:"idcard_pics,omitempty" query:"idcard_pics,omitempty"` 232 | Hosp string `form:"hosp,required" json:"hosp,required" query:"hosp,required"` 233 | HospDept string `form:"hosp_dept,required" json:"hosp_dept,required" query:"hosp_dept,required"` 234 | HospProv *string `form:"hosp_prov,omitempty" json:"hosp_prov,omitempty" query:"hosp_prov,omitempty"` 235 | HospCity *string `form:"hosp_city,omitempty" json:"hosp_city,omitempty" query:"hosp_city,omitempty"` 236 | HospCounty *string `form:"hosp_county,omitempty" json:"hosp_county,omitempty" query:"hosp_county,omitempty"` 237 | ProTit string `form:"pro_tit,required" json:"pro_tit,required" query:"pro_tit,required"` 238 | ThTit *string `form:"th_tit,omitempty" json:"th_tit,omitempty" query:"th_tit,omitempty"` 239 | ServDepts *string `form:"serv_depts,omitempty" json:"serv_depts,omitempty" query:"serv_depts,omitempty"` 240 | TitCerts []string `form:"tit_certs,omitempty" json:"tit_certs,omitempty" query:"tit_certs,omitempty"` 241 | ThTitCerts []string `form:"th_tit_certs,omitempty" json:"th_tit_certs,omitempty" query:"th_tit_certs,omitempty"` 242 | PracCerts []string `form:"prac_certs,omitempty" json:"prac_certs,omitempty" query:"prac_certs,omitempty"` 243 | QualCerts []string `form:"qual_certs,omitempty" json:"qual_certs,omitempty" query:"qual_certs,omitempty"` 244 | PracCertNo string `form:"prac_cert_no,required" json:"prac_cert_no,required" query:"prac_cert_no,required" vd:"len($)==15"` 245 | Goodat *string `form:"goodat,omitempty" json:"goodat,omitempty" query:"goodat,omitempty"` 246 | Intro *string `form:"intro,omitempty" json:"intro,omitempty" query:"intro,omitempty"` 247 | Linkman string `form:"linkman,required" json:"linkman,required" query:"linkman,required" vd:"email($)"` 248 | Phone string `form:"phone,required" json:"phone,required" query:"phone,required" vd:"phone($,'CN')"` 249 | } 250 | 251 | type SubmitDoctorImportRequest struct { 252 | SubmitDoctorImport []*SubmitDoctorImportItem `form:"submit_doctor_import,required" json:"submit_doctor_import,required"` 253 | } 254 | var data = &SubmitDoctorImportRequest{SubmitDoctorImport: []*SubmitDoctorImportItem{{}}} 255 | err := vd.Validate(data, true) 256 | assert.EqualError(t, err, "invalid parameter: SubmitDoctorImport[0].Idcard\tinvalid parameter: SubmitDoctorImport[0].PracCertNo\temail format is incorrect\tthe phone number supplied is not a number") 257 | } 258 | 259 | func TestStructSliceMap(t *testing.T) { 260 | type F struct { 261 | f struct { 262 | g int `vd:"$%3==0"` 263 | } 264 | } 265 | f := &F{} 266 | f.f.g = 10 267 | type S struct { 268 | A map[string]*F 269 | B []map[string]*F 270 | C map[string][]map[string]F 271 | // _ int 272 | } 273 | s := S{ 274 | A: map[string]*F{"x": f}, 275 | B: []map[string]*F{{"y": f}}, 276 | C: map[string][]map[string]F{"z": {{"zz": *f}}}, 277 | } 278 | err := vd.Validate(s, true) 279 | assert.EqualError(t, err, "invalid parameter: A{v for k=x}.f.g\tinvalid parameter: B[0]{v for k=y}.f.g\tinvalid parameter: C{v for k=z}[0]{v for k=zz}.f.g") 280 | } 281 | 282 | func TestIssue30(t *testing.T) { 283 | type TStruct struct { 284 | TOk string `vd:"gt($,'0') && gt($, '1')" json:"t_ok"` 285 | // TFail string `vd:"gt($,'0')" json:"t_fail"` 286 | } 287 | vd.RegFunc("gt", func(args ...interface{}) error { 288 | return errors.New("force error") 289 | }) 290 | assert.EqualError(t, vd.Validate(&TStruct{TOk: "1"}), "invalid parameter: TOk") 291 | // assert.NoError(t, vd.Validate(&TStruct{TOk: "1", TFail: "1"})) 292 | } 293 | 294 | func TestIssue31(t *testing.T) { 295 | type TStruct struct { 296 | A []int32 `vd:"$ == nil || ($ != nil && range($, in(#v, 1, 2, 3))"` 297 | } 298 | assert.EqualError(t, vd.Validate(&TStruct{A: []int32{1}}), "syntax error: \"($ != nil && range($, in(#v, 1, 2, 3))\"") 299 | assert.EqualError(t, vd.Validate(&TStruct{A: []int32{1}}), "syntax error: \"($ != nil && range($, in(#v, 1, 2, 3))\"") 300 | assert.EqualError(t, vd.Validate(&TStruct{A: []int32{1}}), "syntax error: \"($ != nil && range($, in(#v, 1, 2, 3))\"") 301 | } 302 | 303 | func TestRegexp(t *testing.T) { 304 | type TStruct struct { 305 | A string `vd:"regexp('(\\d+\\.){3}\\d+')"` 306 | } 307 | assert.NoError(t, vd.Validate(&TStruct{A: "0.0.0.0"})) 308 | assert.EqualError(t, vd.Validate(&TStruct{A: "0...0"}), "invalid parameter: A") 309 | assert.EqualError(t, vd.Validate(&TStruct{A: "abc1"}), "invalid parameter: A") 310 | assert.EqualError(t, vd.Validate(&TStruct{A: "0?0?0?0"}), "invalid parameter: A") 311 | } 312 | 313 | func TestRangeIn(t *testing.T) { 314 | type S struct { 315 | F []string `vd:"range($, in(#v, '', 'ttp', 'euttp'))"` 316 | } 317 | err := vd.Validate(S{ 318 | F: []string{"ttp", "", "euttp"}, 319 | }) 320 | assert.NoError(t, err) 321 | err = vd.Validate(S{ 322 | F: []string{"ttp", "?", "euttp"}, 323 | }) 324 | assert.EqualError(t, err, "invalid parameter: F") 325 | } 326 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Bytedance Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /binding/bind.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | jsonpkg "encoding/json" 5 | "net/http" 6 | "reflect" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/andeya/ameda" 11 | "github.com/andeya/goutil" 12 | 13 | "github.com/bytedance/go-tagexpr/v2" 14 | "github.com/bytedance/go-tagexpr/v2/validator" 15 | ) 16 | 17 | // Binding binding and verification tool for http request 18 | type Binding struct { 19 | vd *validator.Validator 20 | recvs map[uintptr]*receiver 21 | lock sync.RWMutex 22 | bindErrFactory func(failField, msg string) error 23 | config Config 24 | jsonUnmarshalFunc func(data []byte, v interface{}) error 25 | } 26 | 27 | // New creates a binding tool. 28 | // NOTE: 29 | // 30 | // Use default tag name for config fields that are empty 31 | func New(config *Config) *Binding { 32 | if config == nil { 33 | config = new(Config) 34 | } 35 | b := &Binding{ 36 | recvs: make(map[uintptr]*receiver, 1024), 37 | config: *config, 38 | } 39 | b.config.init() 40 | b.vd = validator.New(b.config.Validator) 41 | return b.SetErrorFactory(nil, nil) 42 | } 43 | 44 | // SetLooseZeroMode if set to true, 45 | // the empty string request parameter is bound to the zero value of parameter. 46 | // NOTE: 47 | // 48 | // The default is false; 49 | // Suitable for these parameter types: query/header/cookie/form . 50 | func (b *Binding) SetLooseZeroMode(enable bool) *Binding { 51 | b.config.LooseZeroMode = enable 52 | for k := range b.recvs { 53 | delete(b.recvs, k) 54 | } 55 | return b 56 | } 57 | 58 | var defaultValidatingErrFactory = newDefaultErrorFactory("validating") 59 | var defaultBindErrFactory = newDefaultErrorFactory("binding") 60 | 61 | // SetErrorFactory customizes the factory of validation error. 62 | // NOTE: 63 | // 64 | // If errFactory==nil, the default is used 65 | func (b *Binding) SetErrorFactory(bindErrFactory, validatingErrFactory func(failField, msg string) error) *Binding { 66 | if bindErrFactory == nil { 67 | bindErrFactory = defaultBindErrFactory 68 | } 69 | if validatingErrFactory == nil { 70 | validatingErrFactory = defaultValidatingErrFactory 71 | } 72 | b.bindErrFactory = bindErrFactory 73 | b.vd.SetErrorFactory(validatingErrFactory) 74 | return b 75 | } 76 | 77 | // BindAndValidate binds the request parameters and validates them if needed. 78 | func (b *Binding) BindAndValidate(recvPointer interface{}, req *http.Request, pathParams PathParams) error { 79 | return b.IBindAndValidate(recvPointer, wrapRequest(req), pathParams) 80 | } 81 | 82 | // Bind binds the request parameters. 83 | func (b *Binding) Bind(recvPointer interface{}, req *http.Request, pathParams PathParams) error { 84 | return b.IBind(recvPointer, wrapRequest(req), pathParams) 85 | } 86 | 87 | // IBindAndValidate binds the request parameters and validates them if needed. 88 | func (b *Binding) IBindAndValidate(recvPointer interface{}, req Request, pathParams PathParams) error { 89 | v, hasVd, err := b.bind(recvPointer, req, pathParams) 90 | if err != nil { 91 | return err 92 | } 93 | if hasVd { 94 | return b.vd.Validate(v) 95 | } 96 | return nil 97 | } 98 | 99 | // IBind binds the request parameters. 100 | func (b *Binding) IBind(recvPointer interface{}, req Request, pathParams PathParams) error { 101 | _, _, err := b.bind(recvPointer, req, pathParams) 102 | return err 103 | } 104 | 105 | // Validate validates whether the fields of value is valid. 106 | func (b *Binding) Validate(value interface{}) error { 107 | return b.vd.Validate(value) 108 | } 109 | 110 | func (b *Binding) bind(pointer interface{}, req Request, pathParams PathParams) (elemValue reflect.Value, hasVd bool, err error) { 111 | elemValue, err = b.receiverValueOf(pointer) 112 | if err != nil { 113 | return 114 | } 115 | if elemValue.Kind() == reflect.Struct { 116 | hasVd, err = b.bindStruct(pointer, elemValue, req, pathParams) 117 | } else { 118 | hasVd, err = b.bindNonstruct(pointer, elemValue, req, pathParams) 119 | } 120 | return 121 | } 122 | 123 | func (b *Binding) bindNonstruct(pointer interface{}, _ reflect.Value, req Request, _ PathParams) (hasVd bool, err error) { 124 | bodyCodec := getBodyCodec(req) 125 | switch bodyCodec { 126 | case bodyJSON: 127 | hasVd = true 128 | bodyBytes, err := req.GetBody() 129 | if err != nil { 130 | return hasVd, err 131 | } 132 | err = b.bindJSON(pointer, bodyBytes) 133 | case bodyProtobuf: 134 | hasVd = true 135 | bodyBytes, err := req.GetBody() 136 | if err != nil { 137 | return hasVd, err 138 | } 139 | err = bindProtobuf(pointer, bodyBytes) 140 | case bodyForm: 141 | postForm, err := req.GetPostForm() 142 | if err != nil { 143 | return false, err 144 | } 145 | b, _ := jsonpkg.Marshal(postForm) 146 | err = jsonpkg.Unmarshal(b, pointer) 147 | default: 148 | // query and form 149 | form, err := req.GetForm() 150 | if err != nil { 151 | return false, err 152 | } 153 | b, _ := jsonpkg.Marshal(form) 154 | err = jsonpkg.Unmarshal(b, pointer) 155 | } 156 | return 157 | } 158 | 159 | func (b *Binding) bindStruct(structPointer interface{}, structValue reflect.Value, req Request, pathParams PathParams) (hasVd bool, err error) { 160 | recv, err := b.getOrPrepareReceiver(structValue) 161 | if err != nil { 162 | return 163 | } 164 | 165 | expr, err := b.vd.VM().Run(structValue) 166 | if err != nil { 167 | return 168 | } 169 | 170 | var bodyString string 171 | bodyCodec, bodyBytes, postForm, fileHeaders, err := recv.getBodyInfo(req) 172 | if len(bodyBytes) > 0 { 173 | err = b.prebindBody(structPointer, structValue, bodyCodec, bodyBytes) 174 | if err != nil { 175 | return 176 | } 177 | bodyString = ameda.UnsafeBytesToString(bodyBytes) 178 | } 179 | queryValues := recv.getQuery(req) 180 | cookies := recv.getCookies(req) 181 | h := recv.getHeader(req) 182 | for _, param := range recv.params { 183 | for i, info := range param.tagInfos { 184 | var found bool 185 | switch info.paramIn { 186 | case raw_body: 187 | err = param.bindRawBody(info, expr, bodyBytes) 188 | found = err == nil 189 | case path: 190 | found, err = param.bindPath(info, expr, pathParams) 191 | case query: 192 | found, err = param.bindQuery(info, expr, queryValues) 193 | case cookie: 194 | found, err = param.bindCookie(info, expr, cookies) 195 | case header: 196 | found, err = param.bindHeader(info, expr, h) 197 | case form, json, protobuf: 198 | if info.paramIn == in(bodyCodec) { 199 | found, err = param.bindOrRequireBody(info, expr, bodyCodec, bodyString, postForm, fileHeaders, 200 | recv.hasDefaultVal) 201 | } else if info.required { 202 | found = false 203 | err = info.requiredError 204 | } 205 | case default_val: 206 | found, err = param.bindDefaultVal(expr, param.defaultVal) 207 | } 208 | if found && err == nil { 209 | break 210 | } 211 | if (found || i == len(param.tagInfos)-1) && err != nil { 212 | return recv.hasVd, err 213 | } 214 | } 215 | } 216 | return recv.hasVd, nil 217 | } 218 | 219 | func (b *Binding) receiverValueOf(receiver interface{}) (reflect.Value, error) { 220 | v := reflect.ValueOf(receiver) 221 | if v.Kind() == reflect.Ptr { 222 | v = ameda.DereferencePtrValue(v) 223 | if v.IsValid() && v.CanAddr() { 224 | return v, nil 225 | } 226 | } 227 | return v, b.bindErrFactory("", "receiver must be a non-nil pointer") 228 | } 229 | 230 | func (b *Binding) getOrPrepareReceiver(value reflect.Value) (*receiver, error) { 231 | runtimeTypeID := ameda.ValueFrom(value).RuntimeTypeID() 232 | b.lock.RLock() 233 | recv, ok := b.recvs[runtimeTypeID] 234 | b.lock.RUnlock() 235 | if ok { 236 | return recv, nil 237 | } 238 | t := value.Type() 239 | expr, err := b.vd.VM().Run(reflect.New(t).Elem()) 240 | if err != nil { 241 | return nil, err 242 | } 243 | recv = &receiver{ 244 | params: make([]*paramInfo, 0, 16), 245 | looseZeroMode: b.config.LooseZeroMode, 246 | } 247 | var errExprSelector tagexpr.ExprSelector 248 | var errMsg string 249 | var fieldsWithValidTag = make(map[string]bool) 250 | expr.RangeFields(func(fh *tagexpr.FieldHandler) bool { 251 | if !fh.Value(true).CanSet() { 252 | selector := fh.StringSelector() 253 | errMsg = "field cannot be set: " + selector 254 | errExprSelector = tagexpr.ExprSelector(selector) 255 | return true 256 | } 257 | 258 | tagKVs := b.config.parse(fh.StructField()) 259 | p := recv.getOrAddParam(fh, b.bindErrFactory) 260 | tagInfos := [maxIn]*tagInfo{} 261 | L: 262 | for _, tagKV := range tagKVs { 263 | paramIn := undefined 264 | switch tagKV.name { 265 | case b.config.Validator: 266 | recv.hasVd = true 267 | continue L 268 | case b.config.PathParam: 269 | paramIn = path 270 | case b.config.FormBody: 271 | paramIn = form 272 | case b.config.Query: 273 | paramIn = query 274 | case b.config.Cookie: 275 | paramIn = cookie 276 | case b.config.Header: 277 | paramIn = header 278 | case b.config.protobufBody: 279 | paramIn = protobuf 280 | case b.config.jsonBody: 281 | paramIn = json 282 | case b.config.RawBody: 283 | paramIn = raw_body 284 | case b.config.defaultVal: 285 | paramIn = default_val 286 | default: 287 | continue L 288 | } 289 | if paramIn == default_val { 290 | tagInfos[paramIn] = &tagInfo{paramIn: default_val, paramName: tagKV.value} 291 | } else { 292 | tagInfos[paramIn] = tagKV.toInfo(paramIn == header) 293 | } 294 | } 295 | 296 | for i, info := range tagInfos { 297 | if info != nil { 298 | if info.paramIn != default_val && info.paramName == "-" { 299 | p.omitIns[in(i)] = true 300 | recv.assginIn(in(i), false) 301 | } else { 302 | info.paramIn = in(i) 303 | p.tagInfos = append(p.tagInfos, info) 304 | recv.assginIn(in(i), true) 305 | } 306 | } 307 | } 308 | fs := string(fh.FieldSelector()) 309 | switch len(p.tagInfos) { 310 | case 0: 311 | var canDefault = true 312 | for s := range fieldsWithValidTag { 313 | if strings.HasPrefix(fs, s) { 314 | canDefault = false 315 | break 316 | } 317 | } 318 | if canDefault { 319 | if !goutil.IsExportedName(p.structField.Name) { 320 | canDefault = false 321 | } 322 | } 323 | // Supports the default binding order when there is no valid tag in the superior field of the exportable field 324 | if canDefault { 325 | for _, i := range sortedDefaultIn { 326 | if p.omitIns[i] { 327 | recv.assginIn(i, false) 328 | continue 329 | } 330 | p.tagInfos = append(p.tagInfos, &tagInfo{ 331 | paramIn: i, 332 | paramName: p.structField.Name, 333 | }) 334 | recv.assginIn(i, true) 335 | } 336 | } 337 | case 1: 338 | if p.tagInfos[0].paramIn == default_val { 339 | last := p.tagInfos[0] 340 | p.tagInfos = make([]*tagInfo, 0, len(sortedDefaultIn)+1) 341 | for _, i := range sortedDefaultIn { 342 | if p.omitIns[i] { 343 | recv.assginIn(i, false) 344 | continue 345 | } 346 | p.tagInfos = append(p.tagInfos, &tagInfo{ 347 | paramIn: i, 348 | paramName: p.structField.Name, 349 | }) 350 | recv.assginIn(i, true) 351 | } 352 | p.tagInfos = append(p.tagInfos, last) 353 | } 354 | fallthrough 355 | default: 356 | fieldsWithValidTag[fs+tagexpr.FieldSeparator] = true 357 | } 358 | if !recv.hasVd { 359 | _, recv.hasVd = tagKVs.lookup(b.config.Validator) 360 | } 361 | return true 362 | }) 363 | 364 | if errMsg != "" { 365 | return nil, b.bindErrFactory(errExprSelector.String(), errMsg) 366 | } 367 | if !recv.hasVd { 368 | recv.hasVd, _ = b.findVdTag(ameda.DereferenceType(t), false, 20, map[reflect.Type]bool{}) 369 | } 370 | recv.initParams() 371 | 372 | b.lock.Lock() 373 | b.recvs[runtimeTypeID] = recv 374 | b.lock.Unlock() 375 | 376 | return recv, nil 377 | } 378 | 379 | func (b *Binding) findVdTag(t reflect.Type, inMapOrSlice bool, depth int, exist map[reflect.Type]bool) (hasVd bool, err error) { 380 | if depth <= 0 || exist[t] { 381 | return 382 | } 383 | depth-- 384 | switch t.Kind() { 385 | case reflect.Struct: 386 | exist[t] = true 387 | for i := t.NumField() - 1; i >= 0; i-- { 388 | field := t.Field(i) 389 | if inMapOrSlice { 390 | tagKVs := b.config.parse(field) 391 | for _, tagKV := range tagKVs { 392 | if tagKV.name == b.config.Validator { 393 | return true, nil 394 | } 395 | } 396 | } 397 | hasVd, _ = b.findVdTag(ameda.DereferenceType(field.Type), inMapOrSlice, depth, exist) 398 | if hasVd { 399 | return true, nil 400 | } 401 | } 402 | return false, nil 403 | case reflect.Slice, reflect.Array, reflect.Map: 404 | return b.findVdTag(ameda.DereferenceType(t.Elem()), true, depth, exist) 405 | default: 406 | return false, nil 407 | } 408 | } 409 | 410 | func (b *Binding) bindJSON(pointer interface{}, bodyBytes []byte) error { 411 | if b.jsonUnmarshalFunc != nil { 412 | return b.jsonUnmarshalFunc(bodyBytes, pointer) 413 | } else { 414 | return jsonpkg.Unmarshal(bodyBytes, pointer) 415 | } 416 | } 417 | 418 | func (b *Binding) ResetJSONUnmarshaler(fn JSONUnmarshaler) { 419 | b.jsonUnmarshalFunc = fn 420 | } 421 | -------------------------------------------------------------------------------- /binding/param_info.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | jsonpkg "encoding/json" 5 | "errors" 6 | "fmt" 7 | "mime/multipart" 8 | "net/http" 9 | "net/url" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/andeya/ameda" 15 | "github.com/bytedance/go-tagexpr/v2" 16 | gjson "github.com/bytedance/go-tagexpr/v2/binding/tidwall_gjson" 17 | ) 18 | 19 | const ( 20 | specialChar = "\x07" 21 | ) 22 | 23 | type paramInfo struct { 24 | fieldSelector string 25 | structField reflect.StructField 26 | tagInfos []*tagInfo 27 | omitIns map[in]bool 28 | bindErrFactory func(failField, msg string) error 29 | looseZeroMode bool 30 | defaultVal []byte 31 | } 32 | 33 | func (p *paramInfo) name(_ in) string { 34 | var name string 35 | for _, info := range p.tagInfos { 36 | if info.paramIn == json { 37 | name = info.paramName 38 | break 39 | } 40 | } 41 | if name == "" { 42 | return p.structField.Name 43 | } 44 | return name 45 | } 46 | 47 | func (p *paramInfo) getField(expr *tagexpr.TagExpr, initZero bool) (reflect.Value, error) { 48 | fh, found := expr.Field(p.fieldSelector) 49 | if found { 50 | v := fh.Value(initZero) 51 | if v.IsValid() { 52 | return v, nil 53 | } 54 | } 55 | return reflect.Value{}, nil 56 | } 57 | 58 | func (p *paramInfo) bindRawBody(info *tagInfo, expr *tagexpr.TagExpr, bodyBytes []byte) error { 59 | if len(bodyBytes) == 0 { 60 | if info.required { 61 | return info.requiredError 62 | } 63 | return nil 64 | } 65 | v, err := p.getField(expr, true) 66 | if err != nil || !v.IsValid() { 67 | return err 68 | } 69 | v = ameda.DereferenceValue(v) 70 | switch v.Kind() { 71 | case reflect.Slice: 72 | if v.Type().Elem().Kind() != reflect.Uint8 { 73 | return info.typeError 74 | } 75 | v.Set(reflect.ValueOf(bodyBytes)) 76 | return nil 77 | case reflect.String: 78 | v.Set(reflect.ValueOf(ameda.UnsafeBytesToString(bodyBytes))) 79 | return nil 80 | default: 81 | return info.typeError 82 | } 83 | } 84 | 85 | func (p *paramInfo) bindPath(info *tagInfo, expr *tagexpr.TagExpr, pathParams PathParams) (bool, error) { 86 | if pathParams == nil { 87 | return false, nil 88 | } 89 | r, found := pathParams.Get(info.paramName) 90 | if !found { 91 | if info.required { 92 | return false, info.requiredError 93 | } 94 | return false, nil 95 | } 96 | return true, p.bindStringSlice(info, expr, []string{r}) 97 | } 98 | 99 | func (p *paramInfo) bindQuery(info *tagInfo, expr *tagexpr.TagExpr, queryValues url.Values) (bool, error) { 100 | return p.bindMapStrings(info, expr, queryValues) 101 | } 102 | 103 | func (p *paramInfo) bindHeader(info *tagInfo, expr *tagexpr.TagExpr, header http.Header) (bool, error) { 104 | return p.bindMapStrings(info, expr, header) 105 | } 106 | 107 | func (p *paramInfo) bindCookie(info *tagInfo, expr *tagexpr.TagExpr, cookies []*http.Cookie) (bool, error) { 108 | var r []string 109 | for _, c := range cookies { 110 | if c.Name == info.paramName { 111 | r = append(r, c.Value) 112 | } 113 | } 114 | if len(r) == 0 { 115 | if info.required { 116 | return false, info.requiredError 117 | } 118 | return false, nil 119 | } 120 | return true, p.bindStringSlice(info, expr, r) 121 | } 122 | 123 | func (p *paramInfo) bindOrRequireBody( 124 | info *tagInfo, expr *tagexpr.TagExpr, bodyCodec codec, bodyString string, 125 | postForm map[string][]string, fileHeaders map[string][]*multipart.FileHeader, hasDefaultVal bool) (bool, error) { 126 | switch bodyCodec { 127 | case bodyForm: 128 | found, err := p.bindMapStrings(info, expr, postForm) 129 | if !found { 130 | return p.bindFileHeaders(info, expr, fileHeaders) 131 | } 132 | return found, err 133 | case bodyJSON: 134 | return p.checkRequireJSON(info, expr, bodyString, hasDefaultVal) 135 | case bodyProtobuf: 136 | // It has been checked when binding, no need to check now 137 | return true, nil 138 | // err := p.checkRequireProtobuf(info, expr, false) 139 | // return err == nil, err 140 | default: 141 | return false, info.contentTypeError 142 | } 143 | } 144 | 145 | func (p *paramInfo) checkRequireProtobuf(info *tagInfo, expr *tagexpr.TagExpr, checkOpt bool) error { 146 | if checkOpt && !info.required { 147 | v, err := p.getField(expr, false) 148 | if err != nil || !v.IsValid() { 149 | return info.requiredError 150 | } 151 | } 152 | return nil 153 | } 154 | 155 | func (p *paramInfo) checkRequireJSON(info *tagInfo, expr *tagexpr.TagExpr, bodyString string, hasDefaultVal bool) (bool, error) { 156 | var requiredError error 157 | if info.required { // only return error if it's a required field 158 | requiredError = info.requiredError 159 | } else if !hasDefaultVal { 160 | return true, nil 161 | } 162 | if !gjson.Get(bodyString, info.namePath).Exists() { 163 | idx := strings.LastIndex(info.namePath, ".") 164 | // There should be a superior but it is empty, no error is reported 165 | if idx > 0 && !gjson.Get(bodyString, info.namePath[:idx]).Exists() { 166 | return true, nil 167 | } 168 | return false, requiredError 169 | } 170 | v, err := p.getField(expr, false) 171 | if err != nil || !v.IsValid() { 172 | return false, requiredError 173 | } 174 | return true, nil 175 | } 176 | 177 | var fileHeaderType = reflect.TypeOf(multipart.FileHeader{}) 178 | 179 | func (p *paramInfo) bindFileHeaders(info *tagInfo, expr *tagexpr.TagExpr, fileHeaders map[string][]*multipart.FileHeader) (bool, error) { 180 | r, ok := fileHeaders[info.paramName] 181 | if !ok || len(r) == 0 { 182 | if info.required { 183 | return false, info.requiredError 184 | } 185 | return false, nil 186 | } 187 | v, err := p.getField(expr, true) 188 | if err != nil || !v.IsValid() { 189 | return true, err 190 | } 191 | v = ameda.DereferenceValue(v) 192 | var elemType reflect.Type 193 | isSlice := v.Kind() == reflect.Slice 194 | if isSlice { 195 | elemType = v.Type().Elem() 196 | } else { 197 | elemType = v.Type() 198 | } 199 | var ptrDepth int 200 | for elemType.Kind() == reflect.Ptr { 201 | elemType = elemType.Elem() 202 | ptrDepth++ 203 | } 204 | if elemType != fileHeaderType { 205 | return true, errors.New("parameter type is not (*)multipart.FileHeader struct or slice") 206 | } 207 | if len(r) == 0 || r[0] == nil { 208 | return true, nil 209 | } 210 | if !isSlice { 211 | v.Set(reflect.ValueOf(*r[0])) 212 | return true, nil 213 | } 214 | for _, fileHeader := range r { 215 | v.Set(reflect.Append(v, ameda.ReferenceValue(reflect.ValueOf(fileHeader), ptrDepth-1))) 216 | } 217 | return true, nil 218 | } 219 | 220 | func (p *paramInfo) bindMapStrings(info *tagInfo, expr *tagexpr.TagExpr, values map[string][]string) (bool, error) { 221 | r, ok := values[info.paramName] 222 | if !ok || len(r) == 0 { 223 | if info.required { 224 | return false, info.requiredError 225 | } 226 | return false, nil 227 | } 228 | return true, p.bindStringSlice(info, expr, r) 229 | } 230 | 231 | // NOTE: len(a)>0 232 | func (p *paramInfo) bindStringSlice(info *tagInfo, expr *tagexpr.TagExpr, a []string) error { 233 | v, err := p.getField(expr, true) 234 | if err != nil || !v.IsValid() { 235 | return err 236 | } 237 | 238 | v = ameda.DereferenceValue(v) 239 | if !v.IsValid() { 240 | return nil 241 | } 242 | // we have customized unmarshal defined, we should use it firstly 243 | if fn, exist := typeUnmarshalFuncs[v.Type()]; exist { 244 | vv, err := fn(a[0], p.looseZeroMode) 245 | if err == nil { 246 | v.Set(vv) 247 | return nil 248 | } 249 | return info.typeError 250 | } 251 | 252 | switch v.Kind() { 253 | case reflect.String: 254 | v.SetString(a[0]) 255 | return nil 256 | 257 | case reflect.Bool: 258 | var bol bool 259 | bol, err = strconv.ParseBool(a[0]) 260 | if err == nil || (a[0] == "" && p.looseZeroMode) { 261 | v.SetBool(bol) 262 | return nil 263 | } 264 | case reflect.Float32: 265 | var f float64 266 | f, err = strconv.ParseFloat(a[0], 32) 267 | if err == nil || (a[0] == "" && p.looseZeroMode) { 268 | v.SetFloat(f) 269 | return nil 270 | } 271 | case reflect.Float64: 272 | var f float64 273 | f, err = strconv.ParseFloat(a[0], 64) 274 | if err == nil || (a[0] == "" && p.looseZeroMode) { 275 | v.SetFloat(f) 276 | return nil 277 | } 278 | case reflect.Int64, reflect.Int: 279 | var i int64 280 | i, err = strconv.ParseInt(a[0], 10, 64) 281 | if err == nil || (a[0] == "" && p.looseZeroMode) { 282 | v.SetInt(i) 283 | return nil 284 | } 285 | case reflect.Int32: 286 | var i int64 287 | i, err = strconv.ParseInt(a[0], 10, 32) 288 | if err == nil || (a[0] == "" && p.looseZeroMode) { 289 | v.SetInt(i) 290 | return nil 291 | } 292 | case reflect.Int16: 293 | var i int64 294 | i, err = strconv.ParseInt(a[0], 10, 16) 295 | if err == nil || (a[0] == "" && p.looseZeroMode) { 296 | v.SetInt(i) 297 | return nil 298 | } 299 | case reflect.Int8: 300 | var i int64 301 | i, err = strconv.ParseInt(a[0], 10, 8) 302 | if err == nil || (a[0] == "" && p.looseZeroMode) { 303 | v.SetInt(i) 304 | return nil 305 | } 306 | case reflect.Uint64, reflect.Uint: 307 | var u uint64 308 | u, err = strconv.ParseUint(a[0], 10, 64) 309 | if err == nil || (a[0] == "" && p.looseZeroMode) { 310 | v.SetUint(u) 311 | return nil 312 | } 313 | case reflect.Uint32: 314 | var u uint64 315 | u, err = strconv.ParseUint(a[0], 10, 32) 316 | if err == nil || (a[0] == "" && p.looseZeroMode) { 317 | v.SetUint(u) 318 | return nil 319 | } 320 | case reflect.Uint16: 321 | var u uint64 322 | u, err = strconv.ParseUint(a[0], 10, 16) 323 | if err == nil || (a[0] == "" && p.looseZeroMode) { 324 | v.SetUint(u) 325 | return nil 326 | } 327 | case reflect.Uint8: 328 | var u uint64 329 | u, err = strconv.ParseUint(a[0], 10, 8) 330 | if err == nil || (a[0] == "" && p.looseZeroMode) { 331 | v.SetUint(u) 332 | return nil 333 | } 334 | case reflect.Slice: 335 | var ptrDepth int 336 | t := v.Type().Elem() 337 | elemKind := t.Kind() 338 | for elemKind == reflect.Ptr { 339 | t = t.Elem() 340 | elemKind = t.Kind() 341 | ptrDepth++ 342 | } 343 | val := reflect.New(v.Type()).Elem() 344 | for _, s := range a { 345 | var vv reflect.Value 346 | vv, err = stringToValue(t, s, p.looseZeroMode) 347 | if err != nil { 348 | break 349 | } 350 | val = reflect.Append(val, ameda.ReferenceValue(vv, ptrDepth)) 351 | } 352 | if err == nil { 353 | v.Set(val) 354 | return nil 355 | } 356 | fallthrough 357 | default: 358 | // no customized unmarshal defined 359 | err = unmarshal(ameda.UnsafeStringToBytes(a[0]), v.Addr().Interface()) 360 | if err == nil { 361 | return nil 362 | } 363 | } 364 | return info.typeError 365 | } 366 | 367 | func (p *paramInfo) bindDefaultVal(expr *tagexpr.TagExpr, defaultValue []byte) (bool, error) { 368 | if defaultValue == nil { 369 | return false, nil 370 | } 371 | v, err := p.getField(expr, true) 372 | if err != nil || !v.IsValid() { 373 | return false, err 374 | } 375 | return true, jsonpkg.Unmarshal(defaultValue, v.Addr().Interface()) 376 | } 377 | 378 | // setDefaultVal preprocess the default tags and store the parsed value 379 | func (p *paramInfo) setDefaultVal() error { 380 | for _, info := range p.tagInfos { 381 | if info.paramIn != default_val { 382 | continue 383 | } 384 | 385 | defaultVal := info.paramName 386 | st := ameda.DereferenceType(p.structField.Type) 387 | switch st.Kind() { 388 | case reflect.String: 389 | p.defaultVal, _ = jsonpkg.Marshal(defaultVal) 390 | continue 391 | case reflect.Slice, reflect.Array, reflect.Map, reflect.Struct: 392 | // escape single quote and double quote, replace single quote with double quote 393 | defaultVal = strings.Replace(defaultVal, `"`, `\"`, -1) 394 | defaultVal = strings.Replace(defaultVal, `\'`, specialChar, -1) 395 | defaultVal = strings.Replace(defaultVal, `'`, `"`, -1) 396 | defaultVal = strings.Replace(defaultVal, specialChar, `'`, -1) 397 | } 398 | p.defaultVal = ameda.UnsafeStringToBytes(defaultVal) 399 | } 400 | return nil 401 | } 402 | 403 | func stringToValue(elemType reflect.Type, s string, emptyAsZero bool) (v reflect.Value, err error) { 404 | v = reflect.New(elemType).Elem() 405 | 406 | // we have customized unmarshal defined, we should use it firstly 407 | if fn, exist := typeUnmarshalFuncs[elemType]; exist { 408 | vv, err := fn(s, emptyAsZero) 409 | if err == nil { 410 | v.Set(vv) 411 | } 412 | return v, err 413 | } 414 | 415 | switch elemType.Kind() { 416 | case reflect.String: 417 | v.SetString(s) 418 | case reflect.Bool: 419 | var i bool 420 | i, err = ameda.StringToBool(s, emptyAsZero) 421 | if err == nil { 422 | v.SetBool(i) 423 | } 424 | case reflect.Float32, reflect.Float64: 425 | var i float64 426 | i, err = ameda.StringToFloat64(s, emptyAsZero) 427 | if err == nil { 428 | v.SetFloat(i) 429 | } 430 | case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: 431 | var i int64 432 | i, err = ameda.StringToInt64(s, emptyAsZero) 433 | if err == nil { 434 | v.SetInt(i) 435 | } 436 | case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: 437 | var i uint64 438 | i, err = ameda.StringToUint64(s, emptyAsZero) 439 | if err == nil { 440 | v.SetUint(i) 441 | } 442 | default: 443 | // no customized unmarshal defined 444 | err = unmarshal(ameda.UnsafeStringToBytes(s), v.Addr().Interface()) 445 | } 446 | if err != nil { 447 | return reflect.Value{}, fmt.Errorf("type mismatch, error=%v", err) 448 | } 449 | return v, nil 450 | } 451 | --------------------------------------------------------------------------------