Skip to content

Commit

Permalink
Support validating map entries (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
tiendc authored Nov 21, 2024
1 parent a799149 commit ca92bec
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 0 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ go get github.com/tiendc/go-validator
Rank string
WorkEmail string
Projects []string
TaskMap map[string]Task
}
var p Person

Expand Down Expand Up @@ -91,6 +92,16 @@ go get github.com/tiendc/go-validator
)
}),

// Validate map entries
vld.Map(p.TaskMap).ForEach(func(k string, v Task, validator ItemValidator) {
validator.Validate(
vld.StrLen(&v.Name, 10, 30).OnError(
vld.SetField(fmt.Sprintf("taskMap[%s].name", k), nil),
vld.SetCustomKey("ERR_VLD_TASK_NAME_INVALID"),
),
)
}),

// OTHER FUNCTIONS
// Pass if at least one of the validations passes
vld.OneOf(
Expand Down
5 changes: 5 additions & 0 deletions validator_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ const (
mapType = "map"
)

// Map allows validating every map entry
func Map[K comparable, V any, M ~map[K]V](m M) MapContentValidator[K, V, M] {
return NewMapContentValidator(m)
}

// MapLen validates the input map must have length in the specified range
func MapLen[K comparable, V any, M ~map[K]V](m M, min, max int) SingleValidator {
return call3[M]("len", mapType, "Min", "Max", mapFunc.Len[K, V, M])(m, min, max)
Expand Down
81 changes: 81 additions & 0 deletions validator_map_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package validation

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -69,3 +70,83 @@ func Test_MapValueUnique(t *testing.T) {
errs = MapValueUnique(map[int]int{3: 1, 2: 2, 1: 1}).Validate(ctxBg)
assert.Equal(t, "value_unique", errs[0].Type())
}

func Test_MapContent_Validate(t *testing.T) {
t.Run("nil/empty map", func(t *testing.T) {
// Nil map
errs := Map(map[int]string(nil)).ForEach(func(k int, v string, validator ItemValidator) {
validator.Validate(StrLen(&v, 1, 10))
}).Validate(ctxBg)
assert.Equal(t, 0, len(errs))

// Empty map
errs = Map(map[int]string{}).ForEach(func(k int, v string, validator ItemValidator) {
validator.Validate(StrLen(&v, 1, 10))
}).Validate(ctxBg)
assert.Equal(t, 0, len(errs))
})

t.Run("validate entries", func(t *testing.T) {
// Validate entries
errs := Map(map[int]int{3: 3, 2: 2, 1: 1}).ForEach(func(k int, v int, validator ItemValidator) {
validator.Validate(
NumGTE(&v, 1),
)
}).Validate(ctxBg)
assert.Equal(t, 0, len(errs))

// Validate entries with errors
errs = Map(map[int]int{3: 3, 2: 2, 1: 1}).ForEach(func(k int, v int, validator ItemValidator) {
validator.Validate(
NumGT(&v, 2).OnError(
SetField(fmt.Sprintf("map[%d]", k), nil),
SetCustomKey("ERR_VLD_MAP_ENTRY_INVALID"),
),
)
}).OnError().Validate(ctxBg)
assert.Equal(t, 2, len(errs))
assert.Equal(t, "gt", errs[0].Type())
assert.Equal(t, "gt", errs[1].Type())
})

t.Run("validate entries with Group", func(t *testing.T) {
errs := Map(map[int]int{3: 3, 2: 2, 1: 1}).ForEach(func(k int, v int, validator ItemValidator) {
validator.Group(
NumGTE(&v, 1),
NumLTE(&v, 2),
)
}).Validate(ctxBg)
assert.Equal(t, 1, len(errs))
assert.Equal(t, "group", errs[0].Type())
})

t.Run("validate entries with OneOf", func(t *testing.T) {
errs := Map(map[int]int{3: 3, 2: 2, 1: 1}).ForEach(func(k int, v int, validator ItemValidator) {
validator.OneOf(
NumLTE(&v, 2),
NumGTE(&v, 1),
)
}).Validate(ctxBg)
assert.Equal(t, 0, len(errs))
})

t.Run("validate entries with ExactOneOf", func(t *testing.T) {
errs := Map(map[int]int{3: 3, 2: 2, 1: 1}).ForEach(func(k int, v int, validator ItemValidator) {
validator.ExactOneOf(
NumGTE(&v, 1),
NumLTE(&v, 2),
)
}).Validate(ctxBg)
assert.Equal(t, 2, len(errs)) // slice[1] and slice[2] not satisfy
})

t.Run("validate entries with NotOf", func(t *testing.T) {
errs := Map(map[int]int{3: 3, 2: 2, 1: 1}).ForEach(func(k int, v int, validator ItemValidator) {
validator.NotOf(
NumGTE(&v, 1),
NumLTE(&v, 2),
)
}).Validate(ctxBg)
assert.Equal(t, 3, len(errs))
})
}
40 changes: 40 additions & 0 deletions validator_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,43 @@ func (c *sliceContentValidator[T, S]) Validate(ctx context.Context) Errors {
errs := execValidators(ctx, elemValidator.get(), false)
return c.applyErrModsWithGrouping(errs)
}

// MapContentValidator validator that validates map entries
type MapContentValidator[K comparable, V any, M ~map[K]V] interface {
Validator
ForEach(fn func(k K, v V, entryValidator ItemValidator)) MapContentValidator[K, V, M]
}

// mapContentValidator implementation of MapContentValidator
type mapContentValidator[K comparable, V any, M ~map[K]V] struct {
baseValidator
mapObj M
entryValidatorFunc func(K, V, ItemValidator)
}

// NewMapContentValidator creates a new MapContentValidator
func NewMapContentValidator[K comparable, V any, M ~map[K]V](mapObj M) MapContentValidator[K, V, M] {
return &mapContentValidator[K, V, M]{mapObj: mapObj}
}

func (m *mapContentValidator[K, V, M]) ForEach(fn func(K, V, ItemValidator)) MapContentValidator[K, V, M] {
m.entryValidatorFunc = fn
return m
}

func (m *mapContentValidator[K, V, M]) OnError(errMods ...ErrorMod) Validator {
m.errMods = errMods
return m
}

func (m *mapContentValidator[K, V, M]) Validate(ctx context.Context) Errors {
if len(m.mapObj) == 0 {
return nil
}
entryValidator := &itemValidator{}
for k, v := range m.mapObj {
m.entryValidatorFunc(k, v, entryValidator)
}
errs := execValidators(ctx, entryValidator.get(), false)
return m.applyErrModsWithGrouping(errs)
}

0 comments on commit ca92bec

Please sign in to comment.