From 3a2cb104a45214a39160ca4e069be188a56cef6c Mon Sep 17 00:00:00 2001 From: RussellLuo Date: Tue, 22 Mar 2022 11:46:25 +0800 Subject: [PATCH] Improve several validator factories - Enhance `Map` and `Slice` by leveraging Go generics - Add `Nested`, and remove redundant `Lazy` - Remove `EachMapValue` (use `Map` instead) - Remove `Each` (use `Slice` instead) - Remove `Assert` (use `Is` instead) --- README.md | 11 +- builtin.go | 112 +++---- builtin_test.go | 401 +++++++++++++------------- example_nested_struct_map_test.go | 4 +- example_nested_struct_pointer_test.go | 10 +- example_nested_struct_slice_test.go | 4 +- example_nested_struct_test.go | 12 +- example_simple_map_test.go | 18 +- example_simple_slice_test.go | 15 +- 9 files changed, 263 insertions(+), 324 deletions(-) diff --git a/README.md b/README.md index edc1a4d..e65bf41 100644 --- a/README.md +++ b/README.md @@ -42,15 +42,12 @@ A validator factory is a function used to create a validator, which will do the - [Func](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Func) - [Schema](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Schema) - [Value](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Value) -- [Slice/Array](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Slice) +- [Nested](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Nested) - [Map](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Map) -- [Each](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Each) -- [EachMapValue](https://pkg.go.dev/github.com/RussellLuo/validating/v3#EachMapValue) +- [Slice/Array](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Slice) - [All/And](https://pkg.go.dev/github.com/RussellLuo/validating/v3#All) - [Any/Or](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Any) - [Not](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Not) -- [Lazy](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Lazy) -- [Assert](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Assert) - [Is](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Is) - [Nonzero](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Nonzero) - [Zero](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Zero) @@ -75,8 +72,8 @@ A validator factory is a function used to create a validator, which will do the ### Validator customizations -- [From a boolean expression](example_nested_struct_pointer_test.go#L24) -- [From a function](example_customizations_test.go#L30-L32) +- [From a boolean function](example_nested_struct_pointer_test.go#L24) +- [From a normal function](example_customizations_test.go#L30-L32) - [From a struct](example_customizations_test.go#L35-L37) diff --git a/builtin.go b/builtin.go index 31d2414..94d1b36 100644 --- a/builtin.go +++ b/builtin.go @@ -35,34 +35,32 @@ func Value(value interface{}, validator Validator) Schema { } } -// Slice is a composite validator factory used to create a validator, which will -// do the validation per the schemas associated with a slice. -func Slice(f func() []Schema) Validator { - schemas := f() - return Func(func(field *Field) (errs Errors) { - for i, s := range schemas { - err := validateSchema(s, field, func(name string) string { - return name + "[" + strconv.Itoa(i) + "]" - }) - if err != nil { - errs.Append(err...) - } +// Nested is a composite validator factory used to create a validator, which will +// delegate the actual validation to the schema returned by f. +func Nested[T any](f func(T) Validator) Validator { + return Func(func(field *Field) Errors { + v, ok := field.Value.(T) + if !ok { + return NewUnsupportedErrors(field, "Nested") } - return + + return f(v).Validate(field) }) } -// Array is an alias of Slice. -var Array = Slice - // Map is a composite validator factory used to create a validator, which will -// do the validation per the schemas associated with a map. -func Map(f func() map[string]Schema) Validator { - schemas := f() +// do the validation per the key/value schemas associated with a map. +func Map[T map[K]V, K comparable, V any](f func(T) map[K]Schema) Validator { return Func(func(field *Field) (errs Errors) { + v, ok := field.Value.(T) + if !ok { + return NewUnsupportedErrors(field, "Map") + } + + schemas := f(v) for k, s := range schemas { err := validateSchema(s, field, func(name string) string { - return name + "[" + k + "]" + return name + fmt.Sprintf("[%v]", k) }) if err != nil { errs.Append(err...) @@ -72,49 +70,31 @@ func Map(f func() map[string]Schema) Validator { }) } -// Each is a composite validator factory used to create a validator, which will -// succeed only when validator succeeds on all elements of the slice field. -func Each[T ~[]E, E any](validator Validator) Validator { +// Slice is a composite validator factory used to create a validator, which will +// do the validation per the schemas associated with a slice. +func Slice[T ~[]E, E any](f func(T) []Schema) Validator { return Func(func(field *Field) (errs Errors) { v, ok := field.Value.(T) if !ok { - return NewUnsupportedErrors(field, "Each") + return NewUnsupportedErrors(field, "Slice") } - for i, vv := range v { - schema := Value((interface{})(vv), validator) - err := validateSchema(schema, field, func(name string) string { + schemas := f(v) + for i, s := range schemas { + err := validateSchema(s, field, func(name string) string { return name + "[" + strconv.Itoa(i) + "]" }) if err != nil { errs.Append(err...) } } - return }) } -// EachMapValue is a composite validator factory used to create a validator, which will -// succeed only when validator succeeds on all values of the map field. -func EachMapValue[T map[K]V, K comparable, V any](validator Validator) Validator { - return Func(func(field *Field) (errs Errors) { - m, ok := field.Value.(T) - if !ok { - return NewUnsupportedErrors(field, "EachKeyValue") - } - - for k, v := range m { - schema := Value((interface{})(v), validator) - err := validateSchema(schema, field, func(name string) string { - return name + fmt.Sprintf("[%v]", k) - }) - if err != nil { - errs.Append(err...) - } - } - return - }) +// Array is an alias of Slice. +func Array[T ~[]E, E any](f func(T) []Schema) Validator { + return Slice[T](f) } // MessageValidator is a validator that allows users to customize the INVALID @@ -219,30 +199,6 @@ func Not(validator Validator) (mv *MessageValidator) { return } -// Lazy is a composite validator factory used to create a validator, which will -// call f only as needed, to delegate the actual validation to -// the validator returned by f. -func Lazy(f func() Validator) Validator { - return Func(func(field *Field) Errors { - return f().Validate(field) - }) -} - -// Assert is a leaf validator factory used to create a validator, which will -// succeed only when the boolean expression evaluates to true. -func Assert(b bool) (mv *MessageValidator) { - mv = &MessageValidator{ - Message: "is invalid", - Validator: Func(func(field *Field) Errors { - if !b { - return NewErrors(field.Name, ErrInvalid, mv.Message) - } - return nil - }), - } - return -} - // Is is a leaf validator factory used to create a validator, which will // succeed when the predicate function f returns true for the field's value. func Is[T any](f func(T) bool) (mv *MessageValidator) { @@ -363,12 +319,12 @@ func RuneCount(min, max int) (mv *MessageValidator) { Validator: Func(func(field *Field) Errors { valid := false - switch t := field.Value.(type) { + switch v := field.Value.(type) { case string: - l := utf8.RuneCountInString(t) + l := utf8.RuneCountInString(v) valid = l >= min && l <= max case []byte: - l := utf8.RuneCount(t) + l := utf8.RuneCount(v) valid = l >= min && l <= max default: return NewUnsupportedErrors(field, "RuneCount") @@ -587,11 +543,11 @@ func Match(re *regexp.Regexp) (mv *MessageValidator) { Validator: Func(func(field *Field) Errors { valid := false - switch t := field.Value.(type) { + switch v := field.Value.(type) { case string: - valid = re.MatchString(t) + valid = re.MatchString(v) case []byte: - valid = re.Match(t) + valid = re.Match(v) default: return NewUnsupportedErrors(field, "Match") } diff --git a/builtin_test.go b/builtin_test.go index 6706c36..f4e96ec 100644 --- a/builtin_test.go +++ b/builtin_test.go @@ -21,6 +21,180 @@ func makeErrsMap(errs v.Errors) map[string]v.Error { return formatted } +func TestNested(t *testing.T) { + cases := []struct { + name string + value interface{} + validator v.Validator + errs v.Errors + }{ + { + name: "invalid", + value: struct{ Foo int }{Foo: 0}, + validator: v.Nested(func(s struct{ Foo int }) v.Validator { + return v.Schema{ + v.F("foo", s.Foo): v.Nonzero[int](), + } + }), + errs: v.NewErrors("foo", v.ErrInvalid, "is zero valued"), + }, + { + name: "valid", + value: struct{ Foo int }{Foo: 1}, + validator: v.Nested(func(s struct{ Foo int }) v.Validator { + return v.Schema{ + v.F("foo", s.Foo): v.Nonzero[int](), + } + }), + errs: nil, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + errs := v.Validate(v.Value(c.value, c.validator)) + if !reflect.DeepEqual(makeErrsMap(errs), makeErrsMap(c.errs)) { + t.Errorf("Got (%+v) != Want (%+v)", errs, c.errs) + } + }) + } +} + +func TestMap(t *testing.T) { + type Stat struct { + Count int + } + + cases := []struct { + name string + value interface{} + validator v.Validator + errs v.Errors + }{ + { + name: "nil map", + value: map[string]Stat(nil), + validator: v.Map(func(m map[string]Stat) map[string]v.Schema { + return nil + }), + errs: nil, + }, + { + name: "invalid", + value: map[string]Stat{ + "visitor": {Count: 0}, + "visit": {Count: 0}, + }, + validator: v.Map(func(m map[string]Stat) map[string]v.Schema { + schemas := make(map[string]v.Schema) + for k, s := range m { + schemas[k] = v.Schema{ + v.F("count", s.Count): v.Nonzero[int](), + } + } + return schemas + }), + errs: v.Errors{ + v.NewError("stats[visitor].count", v.ErrInvalid, "is zero valued"), + v.NewError("stats[visit].count", v.ErrInvalid, "is zero valued"), + }, + }, + { + name: "valid", + value: map[string]Stat{ + "visitor": {Count: 1}, + "visit": {Count: 2}, + }, + validator: v.Map(func(m map[string]Stat) map[string]v.Schema { + schemas := make(map[string]v.Schema) + for k, s := range m { + schemas[k] = v.Schema{ + v.F("count", s.Count): v.Nonzero[int](), + } + } + return schemas + }), + errs: nil, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + errs := v.Validate(v.Schema{ + v.F("stats", c.value): c.validator, + }) + if !reflect.DeepEqual(makeErrsMap(errs), makeErrsMap(c.errs)) { + t.Errorf("Got (%+v) != Want (%+v)", errs, c.errs) + } + }) + } +} + +func TestSlice(t *testing.T) { + type Comment struct { + Content string + CreatedAt time.Time + } + + cases := []struct { + name string + value interface{} + validator v.Validator + errs v.Errors + }{ + { + name: "nil slice", + value: []Comment(nil), + validator: v.Slice(func(s []Comment) (schemas []v.Schema) { + return nil + }), + errs: nil, + }, + { + name: "invalid", + value: []Comment{ + {Content: "", CreatedAt: time.Time{}}, + }, + + validator: v.Slice(func(s []Comment) (schemas []v.Schema) { + for _, c := range s { + schemas = append(schemas, v.Schema{ + v.F("content", c.Content): v.Nonzero[string](), + v.F("created_at", c.CreatedAt): v.Nonzero[time.Time](), + }) + } + return + }), + errs: v.Errors{ + v.NewError("comments[0].content", v.ErrInvalid, "is zero valued"), + v.NewError("comments[0].created_at", v.ErrInvalid, "is zero valued"), + }, + }, + { + name: "nil slice", + value: []Comment(nil), + validator: v.Slice(func(s []Comment) (schemas []v.Schema) { + for _, c := range s { + schemas = append(schemas, v.Schema{ + v.F("content", c.Content): v.Nonzero[string](), + v.F("created_at", c.CreatedAt): v.Nonzero[time.Time](), + }) + } + return + }), + errs: nil, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + errs := v.Validate(v.Schema{ + v.F("comments", c.value): c.validator, + }) + if !reflect.DeepEqual(makeErrsMap(errs), makeErrsMap(c.errs)) { + t.Errorf("Got (%+v) != Want (%+v)", errs, c.errs) + } + }) + } +} + func TestAll(t *testing.T) { cases := []struct { schema v.Schema @@ -158,216 +332,47 @@ func TestNot(t *testing.T) { } } -func TestMap(t *testing.T) { - type Stat struct { - Count int - } - - cases := []struct { - schemaMaker func() v.Schema - errs v.Errors - }{ - { - func() v.Schema { - return v.Schema{ - v.F("stats", nil): v.Map(func() map[string]v.Schema { - return nil - }), - } - }, - nil, - }, - { - func() v.Schema { - stats := map[string]Stat{ - "visitor": {Count: 0}, - "visit": {Count: 0}, - } - return v.Schema{ - v.F("stats", stats): v.Map(func() map[string]v.Schema { - schemas := make(map[string]v.Schema) - for k, s := range stats { - schemas[k] = v.Schema{ - v.F("count", s.Count): v.Nonzero[int](), - } - } - return schemas - }), - } - }, - v.Errors{ - v.NewError("stats[visitor].count", v.ErrInvalid, "is zero valued"), - v.NewError("stats[visit].count", v.ErrInvalid, "is zero valued"), - }, - }, - { - func() v.Schema { - stats := map[string]Stat{ - "visitor": {Count: 1}, - "visit": {Count: 2}, - } - return v.Schema{ - v.F("stats", stats): v.Map(func() map[string]v.Schema { - schemas := make(map[string]v.Schema) - for k, s := range stats { - schemas[k] = v.Schema{ - v.F("count", s.Count): v.Nonzero[int](), - } - } - return schemas - }), - } - }, - nil, - }, - } - for _, c := range cases { - errs := v.Validate(c.schemaMaker()) - if !reflect.DeepEqual(makeErrsMap(errs), makeErrsMap(c.errs)) { - t.Errorf("Got (%+v) != Want (%+v)", errs, c.errs) - } - } -} - -func TestSlice(t *testing.T) { - type Comment struct { - Content string - CreatedAt time.Time - } - - cases := []struct { - schemaMaker func() v.Schema - errs v.Errors - }{ - { - func() v.Schema { - return v.Schema{ - v.F("comments", nil): v.Slice(func() []v.Schema { - return nil - }), - } - }, - nil, - }, - { - func() v.Schema { - comments := []Comment{ - {Content: "", CreatedAt: time.Time{}}, - } - return v.Schema{ - v.F("comments", comments): v.Slice(func() (schemas []v.Schema) { - for _, c := range comments { - schemas = append(schemas, v.Schema{ - v.F("content", c.Content): v.Nonzero[string](), - v.F("created_at", c.CreatedAt): v.Nonzero[time.Time](), - }) - } - return - }), - } - }, - v.Errors{ - v.NewError("comments[0].content", v.ErrInvalid, "is zero valued"), - v.NewError("comments[0].created_at", v.ErrInvalid, "is zero valued"), - }, - }, - { - func() v.Schema { - comments := []Comment{ - {Content: "thanks", CreatedAt: time.Now()}, - } - return v.Schema{ - v.F("comments", comments): v.Slice(func() (schemas []v.Schema) { - for _, c := range comments { - schemas = append(schemas, v.Schema{ - v.F("content", c.Content): v.Nonzero[string](), - v.F("created_at", c.CreatedAt): v.Nonzero[time.Time](), - }) - } - return - }), - } - }, - nil, - }, - } - for _, c := range cases { - errs := v.Validate(c.schemaMaker()) - if !reflect.DeepEqual(makeErrsMap(errs), makeErrsMap(c.errs)) { - t.Errorf("Got (%+v) != Want (%+v)", errs, c.errs) - } - } -} - -func TestLazy(t *testing.T) { +func TestIs(t *testing.T) { cases := []struct { - schemaMaker func(*bool) v.Schema - gotFlag bool - wantFlag bool + name string + value interface{} + validator v.Validator + errs v.Errors }{ { - schemaMaker: func(flag *bool) v.Schema { - return v.Schema{ - v.F("title", ""): v.Lazy(func() v.Validator { - *flag = true - return v.LenString(2, 5) - }), - } - }, - wantFlag: true, + name: "int invalid", + value: 0, + validator: v.Is(func(i int) bool { return i == 1 }), + errs: v.NewErrors("value", v.ErrInvalid, "is invalid"), }, { - schemaMaker: func(flag *bool) v.Schema { - return v.Schema{ - v.F("title", ""): v.All( - v.Nonzero[string](), - v.Lazy(func() v.Validator { - *flag = true - return v.LenString(2, 5) - }), - ), - } - }, - wantFlag: false, - }, - } - for _, c := range cases { - v.Validate(c.schemaMaker(&c.gotFlag)) // nolint:errcheck - if !reflect.DeepEqual(c.gotFlag, c.wantFlag) { - t.Errorf("Got (%+v) != Want (%+v)", c.gotFlag, c.wantFlag) - } - } -} - -func TestAssert(t *testing.T) { - cases := []struct { - schema v.Schema - errs v.Errors - }{ - { - v.Schema{ - v.F("value", nil): v.Assert(true), - }, - nil, + name: "int valid", + value: 1, + validator: v.Is(func(i int) bool { return i == 1 }), + errs: nil, }, { - v.Schema{ - v.F("value", nil): v.Assert(false), - }, - v.NewErrors("value", v.ErrInvalid, "is invalid"), + name: "string invalid", + value: "", + validator: v.Is(func(s string) bool { return s == "a" }), + errs: v.NewErrors("value", v.ErrInvalid, "is invalid"), }, { - v.Schema{ - v.F("value", nil): v.Assert(false).Msg("is not ok"), - }, - v.NewErrors("value", v.ErrInvalid, "is not ok"), + name: "string valid", + value: "a", + validator: v.Is(func(s string) bool { return s == "a" }), + errs: nil, }, } for _, c := range cases { - errs := v.Validate(c.schema) - if !reflect.DeepEqual(makeErrsMap(errs), makeErrsMap(c.errs)) { - t.Errorf("Got (%+v) != Want (%+v)", errs, c.errs) - } + t.Run(c.name, func(t *testing.T) { + errs := v.Validate(v.Schema{ + v.F("value", c.value): c.validator, + }) + if !reflect.DeepEqual(makeErrsMap(errs), makeErrsMap(c.errs)) { + t.Errorf("Got (%+v) != Want (%+v)", errs, c.errs) + } + }) } } diff --git a/example_nested_struct_map_test.go b/example_nested_struct_map_test.go index f81ca8a..49135ef 100644 --- a/example_nested_struct_map_test.go +++ b/example_nested_struct_map_test.go @@ -20,9 +20,9 @@ func makeSchema1(p *Person1) v.Schema { return v.Schema{ v.F("name", p.Name): v.LenString(1, 5), v.F("age", p.Age): v.Nonzero[int](), - v.F("family", p.Family): v.Map(func() map[string]v.Schema { + v.F("family", p.Family): v.Map(func(m map[string]*Member) map[string]v.Schema { schemas := make(map[string]v.Schema) - for relation, member := range p.Family { + for relation, member := range m { schemas[relation] = v.Schema{ v.F("name", member.Name): v.LenString(10, 15).Msg("is too long"), } diff --git a/example_nested_struct_pointer_test.go b/example_nested_struct_pointer_test.go index 295ee5e..d749ce9 100644 --- a/example_nested_struct_pointer_test.go +++ b/example_nested_struct_pointer_test.go @@ -21,12 +21,12 @@ func makeSchema2(p *Person2) v.Schema { v.F("name", p.Name): v.LenString(1, 5), v.F("age", p.Age): v.Lte(50), v.F("address", p.Address): v.All( - v.Assert(p.Address != nil).Msg("is nil"), - v.Lazy(func() v.Validator { + v.Is(func(addr *Address2) bool { return addr != nil }).Msg("is nil"), + v.Nested(func(addr *Address2) v.Validator { return v.Schema{ - v.F("country", p.Address.Country): v.Nonzero[string](), - v.F("province", p.Address.Province): v.Nonzero[string](), - v.F("city", p.Address.City): v.Nonzero[string](), + v.F("country", addr.Country): v.Nonzero[string](), + v.F("province", addr.Province): v.Nonzero[string](), + v.F("city", addr.City): v.Nonzero[string](), } }), ), diff --git a/example_nested_struct_slice_test.go b/example_nested_struct_slice_test.go index ca71903..2cfb97f 100644 --- a/example_nested_struct_slice_test.go +++ b/example_nested_struct_slice_test.go @@ -20,8 +20,8 @@ func makeSchema4(p *Person4) v.Schema { return v.Schema{ v.F("name", p.Name): v.LenString(1, 5), v.F("age", p.Age): v.Nonzero[int](), - v.F("phones", p.Phones): v.Slice(func() (schemas []v.Schema) { - for _, phone := range p.Phones { + v.F("phones", p.Phones): v.Slice(func(s []*Phone) (schemas []v.Schema) { + for _, phone := range s { schemas = append(schemas, v.Schema{ v.F("number", phone.Number): v.Nonzero[string](), v.F("remark", phone.Remark): v.LenString(5, 7), diff --git a/example_nested_struct_test.go b/example_nested_struct_test.go index ba15403..e195afd 100644 --- a/example_nested_struct_test.go +++ b/example_nested_struct_test.go @@ -21,11 +21,13 @@ func Example_nestedStruct() { err := v.Validate(v.Schema{ v.F("name", p.Name): v.LenString(1, 5), v.F("age", p.Age): v.Gte(10), - v.F("address", p.Address): v.Schema{ - v.F("country", p.Address.Country): v.Nonzero[string](), - v.F("province", p.Address.Province): v.Nonzero[string](), - v.F("city", p.Address.City): v.Nonzero[string](), - }, + v.F("address", p.Address): v.Nested(func(addr Address) v.Validator { + return v.Schema{ + v.F("country", addr.Country): v.Nonzero[string](), + v.F("province", addr.Province): v.Nonzero[string](), + v.F("city", addr.City): v.Nonzero[string](), + } + }), }) fmt.Printf("err: %+v\n", err) } diff --git a/example_simple_map_test.go b/example_simple_map_test.go index f2b2043..5818b89 100644 --- a/example_simple_map_test.go +++ b/example_simple_map_test.go @@ -11,25 +11,13 @@ func Example_simpleMap() { "foo": 0, "bar": 1, } - err := v.Validate(v.Map(func() map[string]v.Schema { + err := v.Validate(v.Value(ages, v.Map(func(m map[string]int) map[string]v.Schema { schemas := make(map[string]v.Schema) - for name, age := range ages { + for name, age := range m { schemas[name] = v.Value(age, v.Nonzero[int]()) } return schemas - })) - fmt.Printf("%+v\n", err) - - // Output: - // [foo]: INVALID(is zero valued) -} - -func Example_simpleMapEachMapValue() { - ages := map[string]int{ - "foo": 0, - "bar": 1, - } - err := v.Validate(v.Value(ages, v.EachMapValue[map[string]int](v.Nonzero[int]()))) + }))) fmt.Printf("%+v\n", err) // Output: diff --git a/example_simple_slice_test.go b/example_simple_slice_test.go index 599ce3d..9e44f6f 100644 --- a/example_simple_slice_test.go +++ b/example_simple_slice_test.go @@ -8,21 +8,12 @@ import ( func Example_simpleSlice() { names := []string{"", "foo"} - err := v.Validate(v.Slice(func() (schemas []v.Schema) { - for _, name := range names { + err := v.Validate(v.Value(names, v.Slice(func(s []string) (schemas []v.Schema) { + for _, name := range s { schemas = append(schemas, v.Value(name, v.Nonzero[string]())) } return schemas - })) - fmt.Printf("%+v\n", err) - - // Output: - // [0]: INVALID(is zero valued) -} - -func Example_simpleSliceEach() { - names := []string{"", "foo"} - err := v.Validate(v.Value(names, v.Each[[]string](v.Nonzero[string]()))) + }))) fmt.Printf("%+v\n", err) // Output: