Skip to content

Commit

Permalink
Add EachMap and EachSlice
Browse files Browse the repository at this point in the history
  • Loading branch information
RussellLuo committed Jun 26, 2024
1 parent c97eada commit 6de4da4
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 16 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ A validator factory is a function used to create a validator, which will do the
- [Schema](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Schema)
- [Value](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Value)
- [Nested](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Nested)
- [EachMap](https://pkg.go.dev/github.com/RussellLuo/validating/v3#EachMap)
- [EachSlice](https://pkg.go.dev/github.com/RussellLuo/validating/v3#EachSlice)
- [Map](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Map)
- [Slice/Array](https://pkg.go.dev/github.com/RussellLuo/validating/v3#Slice)
- [All/And](https://pkg.go.dev/github.com/RussellLuo/validating/v3#All)
Expand Down
52 changes: 52 additions & 0 deletions builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,58 @@ func Nested[T any](f func(T) Validator) Validator {
})
}

// EachMap is a composite validator factory used to create a validator, which will
// apply the given validator to each element (i.e. the map value) of the map field.
//
// Usually, for simplicity, it's recommended to use EachMap. If you have more
// complex validation rules for map elements, such as different validation for
// each value or validation specific to keys, then you should use Map.
func EachMap[T map[K]V, K comparable, V any](validator Validator) Validator {
return Func(func(field *Field) (errs Errors) {
v, ok := field.Value.(T)
if !ok {
return NewUnsupportedErrors(field, "EachMap")
}

for k := range v {
s := toSchema(v[k], validator)
err := validateSchema(s, field, func(name string) string {
return name + fmt.Sprintf("[%v]", k)
})
if err != nil {
errs.Append(err...)
}
}
return
})
}

// EachSlice is a composite validator factory used to create a validator, which will
// apply the given validator to each element of the slice field.
//
// Usually, for simplicity, it's recommended to use EachSlice. If you have more
// complex validation rules for slice elements, such as different validation for
// each element, then you should use Slice.
func EachSlice[T ~[]E, E any](validator Validator) Validator {
return Func(func(field *Field) (errs Errors) {
v, ok := field.Value.(T)
if !ok {
return NewUnsupportedErrors(field, "EachSlice")
}

for i := range v {
s := toSchema(v[i], validator)
err := validateSchema(s, field, func(name string) string {
return name + "[" + strconv.Itoa(i) + "]"
})
if err != nil {
errs.Append(err...)
}
}
return
})
}

// 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[T map[K]V, K comparable, V any](f func(T) map[K]Validator) Validator {
Expand Down
147 changes: 144 additions & 3 deletions builtin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,146 @@ func TestNested(t *testing.T) {
}
}

func TestEachMap(t *testing.T) {
type Stat struct {
Count int
}

cases := []struct {
name string
value any
validator v.Validator
errs v.Errors
}{
{
name: "nil map",
value: map[string]Stat(nil),
validator: v.EachMap[map[string]Stat](nil),
errs: nil,
},
{
name: "invalid",
value: map[string]Stat{
"visitor": {Count: 0},
"visit": {Count: 0},
},
validator: v.EachMap[map[string]Stat](v.Nested(func(s Stat) v.Validator {
return v.Schema{
v.F("count", s.Count): v.Nonzero[int](),
}
})),
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.EachMap[map[string]Stat](v.Nested(func(s Stat) v.Validator {
return v.Schema{
v.F("count", s.Count): v.Nonzero[int](),
}
})),
errs: nil,
},
{
name: "int map",
value: map[string]int{
"k1": 1,
"k2": 2,
"k3": 3,
},
validator: v.EachMap[map[string]int](v.Range[int](1, 2)),
errs: v.Errors{
v.NewError("stats[k3]", v.ErrInvalid, "is not between the given range"),
},
},
}
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 TestEachSlice(t *testing.T) {
type Comment struct {
Content string
CreatedAt time.Time
}

cases := []struct {
name string
value any
validator v.Validator
errs v.Errors
}{
{
name: "nil slice",
value: []Comment(nil),
validator: v.EachSlice[[]Comment](nil),
errs: nil,
},
{
name: "invalid",
value: []Comment{
{Content: "", CreatedAt: time.Time{}},
},

validator: v.EachSlice[[]Comment](v.Nested(func(c Comment) v.Validator {
return v.Schema{
v.F("content", c.Content): v.Nonzero[string](),
v.F("created_at", c.CreatedAt): v.Nonzero[time.Time](),
}
})),
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: "valid",
value: []Comment{
{Content: "LGTM", CreatedAt: time.Now()},
},
validator: v.EachSlice[[]Comment](v.Nested(func(c Comment) v.Validator {
return v.Schema{
v.F("content", c.Content): v.Nonzero[string](),
v.F("created_at", c.CreatedAt): v.Nonzero[time.Time](),
}
})),
errs: nil,
},
{
name: "int slice",
value: []int{1, 2, 3},
validator: v.EachSlice[[]int](v.Range[int](1, 2)),
errs: v.Errors{
v.NewError("comments[2]", v.ErrInvalid, "is not between the given range"),
},
},
}
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 TestMap(t *testing.T) {
type Stat struct {
Count int
Expand Down Expand Up @@ -171,7 +311,6 @@ func TestSlice(t *testing.T) {
value: []Comment{
{Content: "", CreatedAt: time.Time{}},
},

validator: v.Slice(func(s []Comment) (schemas []v.Validator) {
for _, c := range s {
schemas = append(schemas, v.Schema{
Expand All @@ -187,8 +326,10 @@ func TestSlice(t *testing.T) {
},
},
{
name: "nil slice",
value: []Comment(nil),
name: "valid",
value: []Comment{
{Content: "LGTM", CreatedAt: time.Now()},
},
validator: v.Slice(func(s []Comment) (schemas []v.Validator) {
for _, c := range s {
schemas = append(schemas, v.Schema{
Expand Down
13 changes: 13 additions & 0 deletions example_nested_struct_map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ type Person1 struct {
}

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.EachMap[map[string]*Member](v.Nested(func(member *Member) v.Validator {
return v.Schema{
v.F("name", member.Name): v.LenString(10, 15).Msg("is too long"),
}
})),
}
}

// The equivalent implementation using Map.
func makeSchema1_Map(p *Person1) v.Schema {

Check failure on line 32 in example_nested_struct_map_test.go

View workflow job for this annotation

GitHub Actions / Lint

func `makeSchema1_Map` is unused (unused)
return v.Schema{
v.F("name", p.Name): v.LenString(1, 5),
v.F("age", p.Age): v.Nonzero[int](),
Expand Down
14 changes: 14 additions & 0 deletions example_nested_struct_slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ type Person4 struct {
}

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.EachSlice[[]*Phone](v.Nested(func(phone *Phone) v.Validator {
return v.Schema{
v.F("number", phone.Number): v.Nonzero[string](),
v.F("remark", phone.Remark): v.LenString(5, 7),
}
})),
}
}

// The equivalent implementation using Slice.
func makeSchema4_Slice(p *Person4) v.Schema {

Check failure on line 33 in example_nested_struct_slice_test.go

View workflow job for this annotation

GitHub Actions / Lint

func `makeSchema4_Slice` is unused (unused)
return v.Schema{
v.F("name", p.Name): v.LenString(1, 5),
v.F("age", p.Age): v.Nonzero[int](),
Expand Down
8 changes: 1 addition & 7 deletions example_simple_map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@ func Example_simpleMap() {
"foo": 0,
"bar": 1,
}
err := v.Validate(v.Value(ages, v.Map(func(m map[string]int) map[string]v.Validator {
schemas := make(map[string]v.Validator)
for name := range m {
schemas[name] = v.Nonzero[int]()
}
return schemas
})))
err := v.Validate(v.Value(ages, v.EachMap[map[string]int](v.Nonzero[int]())))
fmt.Printf("%+v\n", err)

// Output:
Expand Down
7 changes: 1 addition & 6 deletions example_simple_slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ import (

func Example_simpleSlice() {
names := []string{"", "foo"}
err := v.Validate(v.Value(names, v.Slice(func(s []string) (schemas []v.Validator) {
for range s {
schemas = append(schemas, v.Nonzero[string]())
}
return schemas
})))
err := v.Validate(v.Value(names, v.EachSlice[[]string](v.Nonzero[string]())))
fmt.Printf("%+v\n", err)

// Output:
Expand Down

0 comments on commit 6de4da4

Please sign in to comment.