diff --git a/.circleci/config.yml b/.circleci/config.yml index 8bc657c..77621cd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,6 +15,16 @@ jobs: docker: - image: circleci/golang:1.11 working_directory: /go/src/github.com/utahta/go-validator + steps: + - checkout + - run: + name: Run test + command: make test + + "go-1.12": + docker: + - image: circleci/golang:1.12 + working_directory: /go/src/github.com/utahta/go-validator steps: - checkout - run: @@ -29,3 +39,4 @@ workflows: jobs: - "go-1.10" - "go-1.11" + - "go-1.12" diff --git a/README.md b/README.md index b775777..44d759d 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,19 @@ go-validator is a data validation library for Go. ```sh go get -u github.com/utahta/go-validator ``` + +# Benchmarks + +3.2 GHz Intel Core i7, 64 GB 2667 MHz DDR4 +```go +goos: darwin +goarch: amd64 +pkg: github.com/utahta/go-validator +BenchmarkValidateVarSuccess-12 20000000 57.5 ns/op 0 B/op 0 allocs/op +BenchmarkValidateVarParallelSuccess-12 100000000 12.8 ns/op 0 B/op 0 allocs/op +BenchmarkValidateStructSuccess-12 10000000 184 ns/op 0 B/op 0 allocs/op +BenchmarkValidateStructParallelSuccess-12 50000000 34.2 ns/op 0 B/op 0 allocs/op +BenchmarkValidateStructComplexSuccess-12 1000000 1072 ns/op 32 B/op 3 allocs/op +BenchmarkValidateStructComplexParallelSuccess-12 10000000 216 ns/op 32 B/op 3 allocs/op +BenchmarkValidateVarFailure-12 10000000 173 ns/op 208 B/op 2 allocs/op +``` diff --git a/benchmark_test.go b/benchmark_test.go index a73d413..826eefa 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -1,6 +1,46 @@ package validator -import "testing" +import ( + "testing" +) + +type ( + I interface { + Foo() string + } + + Impl struct { + F string `valid:"len(3)"` + } + + SubTest struct { + Test string `valid:"required"` + } + + TestString struct { + BlankTag string `valid:""` + Required string `valid:"required"` + Len string `valid:"len(10)"` + Min string `valid:"min(1)"` + Max string `valid:"max(10)"` + MinMax string `valid:"min(1),max(10)"` + Lt string `valid:"max(9)"` + Lte string `valid:"max(10)"` + Gt string `valid:"min(11)"` + Gte string `valid:"min(10)"` + OmitEmpty string `valid:"optional,min(1),max(10)"` + Sub *SubTest + SubIgnore *SubTest `valid:"-"` + Anonymous struct { + A string `valid:"required"` + } + Iface I + } +) + +func (i *Impl) Foo() string { + return i.F +} func BenchmarkValidateVarSuccess(b *testing.B) { v := New() @@ -9,10 +49,27 @@ func BenchmarkValidateVarSuccess(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - v.ValidateVar(&s, "len(1)") + if err := v.ValidateVar(&s, "len(1)"); err != nil { + b.Fatal(err) + } } } +func BenchmarkValidateVarParallelSuccess(b *testing.B) { + v := New() + + s := "1" + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := v.ValidateVar(&s, "len(1)"); err != nil { + b.Fatal(err) + } + } + }) +} + func BenchmarkValidateStructSuccess(b *testing.B) { v := New() @@ -21,10 +78,123 @@ func BenchmarkValidateStructSuccess(b *testing.B) { IntValue int `valid:"len(5|10)"` } - validFoo := &Foo{StringValue: "Foobar", IntValue: 7} + s := &Foo{StringValue: "Foobar", IntValue: 7} + + b.ResetTimer() + for n := 0; n < b.N; n++ { + if err := v.ValidateStruct(s); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkValidateStructParallelSuccess(b *testing.B) { + v := New() + + type Foo struct { + StringValue string `valid:"len(5|10)"` + IntValue int `valid:"len(5|10)"` + } + + s := &Foo{StringValue: "Foobar", IntValue: 7} + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := v.ValidateStruct(s); err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkValidateStructComplexSuccess(b *testing.B) { + v := New() + + s := &TestString{ + Required: "12345", + Len: "1234567890", + Min: "1", + Max: "1234567890", + MinMax: "12345", + Lt: "123456789", + Lte: "1234567890", + Gt: "12345678901", + Gte: "1234567890", + OmitEmpty: "", + Sub: &SubTest{ + Test: "1", + }, + SubIgnore: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `valid:"required"` + }{ + A: "1", + }, + Iface: &Impl{ + F: "123", + }, + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + if err := v.ValidateStruct(s); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkValidateStructComplexParallelSuccess(b *testing.B) { + v := New() + + s := &TestString{ + Required: "12345", + Len: "1234567890", + Min: "1", + Max: "1234567890", + MinMax: "12345", + Lt: "123456789", + Lte: "1234567890", + Gt: "12345678901", + Gte: "1234567890", + OmitEmpty: "", + Sub: &SubTest{ + Test: "1", + }, + SubIgnore: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `valid:"required"` + }{ + A: "1", + }, + Iface: &Impl{ + F: "123", + }, + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := v.ValidateStruct(s); err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkValidateVarFailure(b *testing.B) { + v := New() + + s := "12" b.ResetTimer() for n := 0; n < b.N; n++ { - v.ValidateStruct(validFoo) + if err := v.ValidateVar(&s, "len(1)"); err == nil { + b.Fatal("want invalid argument error, but got nil") + } } } diff --git a/field_cache.go b/field_cache.go index 9084b20..fab4380 100644 --- a/field_cache.go +++ b/field_cache.go @@ -2,8 +2,10 @@ package validator type ( fieldCache struct { + index int isPrivate bool name string tagValue string + tagChunk *tagChunk } ) diff --git a/func.go b/func.go index fe71f0e..7262490 100644 --- a/func.go +++ b/func.go @@ -32,8 +32,8 @@ var ( defaultFuncMap = FuncMap{ "required": hasValue, "req": hasValue, - "empty": zeroValue, - "zero": zeroValue, + "empty": isZeroValue, + "zero": isZeroValue, "alpha": isAlpha, "alphanum": isAlphaNum, "alphaunicode": isAlphaUnicode, @@ -124,7 +124,7 @@ func hasValue(_ context.Context, f Field, _ FuncOption) (bool, error) { return !isEmpty(f), nil } -func zeroValue(_ context.Context, f Field, _ FuncOption) (bool, error) { +func isZeroValue(_ context.Context, f Field, _ FuncOption) (bool, error) { return isEmpty(f), nil } @@ -293,11 +293,11 @@ func minLength(_ context.Context, f Field, opt FuncOption) (bool, error) { v := f.current switch v.Kind() { case reflect.String, reflect.Array, reflect.Map, reflect.Slice: - min, err := strconv.Atoi(minStr) + min, err := strconv.ParseInt(minStr, 10, 64) if err != nil { return false, err } - return min <= v.Len(), nil + return min <= int64(v.Len()), nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: min, err := strconv.ParseInt(minStr, 10, 64) @@ -334,11 +334,11 @@ func maxLength(_ context.Context, f Field, opt FuncOption) (bool, error) { v := f.current switch v.Kind() { case reflect.String, reflect.Array, reflect.Map, reflect.Slice: - max, err := strconv.Atoi(maxStr) + max, err := strconv.ParseInt(maxStr, 10, 64) if err != nil { return false, err } - return v.Len() <= max, nil + return int64(v.Len()) <= max, nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: max, err := strconv.ParseInt(maxStr, 10, 64) @@ -373,11 +373,11 @@ func eqLength(_ context.Context, f Field, opt FuncOption) (bool, error) { v := f.current switch v.Kind() { case reflect.String, reflect.Array, reflect.Map, reflect.Slice: - i, err := strconv.Atoi(str) + i, err := strconv.ParseInt(str, 10, 64) if err != nil { return false, err } - return v.Len() == i, nil + return int64(v.Len()) == i, nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: i, err := strconv.ParseInt(str, 10, 64) diff --git a/tag_parser.go b/tag_parser.go index b22e4ea..c788a7c 100644 --- a/tag_parser.go +++ b/tag_parser.go @@ -5,19 +5,19 @@ import ( "strings" ) -func (v *Validator) tagParse(rawTag string) (*tagChunk, error) { +func (v *Validator) parseTag(rawTag string) (*tagChunk, error) { if tags, ok := v.tagCache.Load(rawTag); ok { return tags, nil } var ( - rootTagChunk tagChunk - chunk *tagChunk - orParsing = false + rootChunk tagChunk + chunk *tagChunk + orParsing = false ) const optionalTagName = "optional" - chunk = &rootTagChunk + chunk = &rootChunk s := newTagScanner(rawTag) loop: @@ -37,7 +37,8 @@ loop: } if orParsing { - chunk.Tags[len(chunk.Tags)-1].Params = append(chunk.Tags[len(chunk.Tags)-1].Params, lit) + idx := len(chunk.Tags) - 1 + chunk.Tags[idx].Params = append(chunk.Tags[idx].Params, lit) } else { tag, err := v.newTag(lit) if err != nil { @@ -56,7 +57,8 @@ loop: } if orParsing { - chunk.Tags[len(chunk.Tags)-1].Params = append(chunk.Tags[len(chunk.Tags)-1].Params, lit) + idx := len(chunk.Tags) - 1 + chunk.Tags[idx].Params = append(chunk.Tags[idx].Params, lit) } else { tag, err := v.newTag(lit) if err != nil { @@ -75,7 +77,8 @@ loop: } if orParsing { - chunk.Tags[len(chunk.Tags)-1].Params = append(chunk.Tags[len(chunk.Tags)-1].Params, lit) + idx := len(chunk.Tags) - 1 + chunk.Tags[idx].Params = append(chunk.Tags[idx].Params, lit) } else { chunk.Tags = append(chunk.Tags, Tag{Name: "or", Params: []string{lit}, validateFn: v.FuncMap["or"]}) } @@ -84,7 +87,8 @@ loop: case nextSeparator: if lit != "" && lit != optionalTagName { if orParsing { - chunk.Tags[len(chunk.Tags)-1].Params = append(chunk.Tags[len(chunk.Tags)-1].Params, lit) + idx := len(chunk.Tags) - 1 + chunk.Tags[idx].Params = append(chunk.Tags[idx].Params, lit) } else { tag, err := v.newTag(lit) if err != nil { @@ -99,9 +103,9 @@ loop: } } - v.tagCache.Store(rawTag, &rootTagChunk) + v.tagCache.Store(rawTag, &rootChunk) - return &rootTagChunk, nil + return &rootChunk, nil } // newTag returns Tag. diff --git a/tag_parser_test.go b/tag_parser_test.go index 647d31f..2ff159b 100644 --- a/tag_parser_test.go +++ b/tag_parser_test.go @@ -308,7 +308,7 @@ func Test_tagParse(t *testing.T) { for _, tc := range testcases { t.Run(tc.rawTag, func(t *testing.T) { - chunk, err := New().tagParse(tc.rawTag) + chunk, err := New().parseTag(tc.rawTag) if err != nil { t.Fatal(err) } @@ -390,7 +390,7 @@ func Test_tagParseInvalid(t *testing.T) { } for _, tc := range testcases { - _, err := New().tagParse(tc.rawTag) + _, err := New().parseTag(tc.rawTag) if err.Error() != tc.wantError { t.Errorf("want `%v`, got `%v`", tc.wantError, err.Error()) } @@ -400,7 +400,7 @@ func Test_tagParseInvalid(t *testing.T) { func Test_tagCache(t *testing.T) { const rawTag = "required,min(1),max(10)" v := New() - want, err := v.tagParse(rawTag) + want, err := v.parseTag(rawTag) if err != nil { t.Fatal(err) } diff --git a/validator.go b/validator.go index 7ddbe00..c46a256 100644 --- a/validator.go +++ b/validator.go @@ -88,25 +88,39 @@ func (v *Validator) validateStruct(ctx context.Context, field Field) error { valueType := val.Type() fieldCaches, hasCache := v.structCache.Load(valueType) - - var errs Errors - for i := 0; i < val.NumField(); i++ { - if !hasCache { + if !hasCache { + for i := 0; i < val.NumField(); i++ { typeField := valueType.Field(i) - fieldCaches = append(fieldCaches, fieldCache{ + cache := fieldCache{ + index: i, isPrivate: typeField.PkgPath != "", // private field tagValue: typeField.Tag.Get(tagName), name: typeField.Name, - }) - } - if fieldCaches[i].isPrivate || fieldCaches[i].tagValue == "-" { - continue + } + if cache.isPrivate { + continue + } + if !v.canValidate(cache.tagValue, v.extractVar(val.Field(i)).Kind()) { + continue + } + + chunk, err := v.parseTag(cache.tagValue) + if err != nil { + return err + } + cache.tagChunk = chunk + + fieldCaches = append(fieldCaches, cache) } + v.structCache.Store(valueType, fieldCaches) + } - originField := val.Field(i) + var errs Errors + for i := 0; i < len(fieldCaches); i++ { + originField := val.Field(fieldCaches[i].index) valueField := v.extractVar(originField) - if err := v.validateVar(ctx, newFieldWithParent(fieldCaches[i].name, originField, valueField, field), fieldCaches[i].tagValue); err != nil { + if err := v.validate(ctx, newFieldWithParent(fieldCaches[i].name, originField, valueField, field), fieldCaches[i].tagChunk); err != nil { if es, ok := err.(Errors); ok { errs = append(errs, es...) } else { @@ -115,10 +129,6 @@ func (v *Validator) validateStruct(ctx context.Context, field Field) error { } } - if !hasCache { - v.structCache.Store(valueType, fieldCaches) - } - if len(errs) > 0 { return errs } @@ -138,28 +148,16 @@ func (v *Validator) ValidateVarContext(ctx context.Context, s interface{}, rawTa } func (v *Validator) validateVar(ctx context.Context, field Field, rawTag string) error { - if rawTag == "-" { + if !v.canValidate(rawTag, field.current.Kind()) { return nil } - chunk, err := v.tagParse(rawTag) + chunk, err := v.parseTag(rawTag) if err != nil { return err } - var errs Errors - if err := v.validate(ctx, field, chunk); err != nil { - if es, ok := err.(Errors); ok { - errs = append(errs, es...) - } else { - return err - } - } - - if len(errs) > 0 { - return errs - } - return nil + return v.validate(ctx, field, chunk) } func (v *Validator) validate(ctx context.Context, field Field, chunk *tagChunk) error { @@ -241,6 +239,24 @@ func (v *Validator) extractVar(in reflect.Value) reflect.Value { } } +func (v *Validator) canValidate(rawTag string, kind reflect.Kind) bool { + if rawTag == "-" { + return false + } + + if rawTag == "" { + switch kind { + case reflect.String, reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64: + // these kinds do not perform recursive process so let's skip validation + return false + } + } + return true +} + // DefaultValidator returns default validator. func DefaultValidator() *Validator { defaultValidatorOnce.Do(func() {