Skip to content

Commit

Permalink
introduce EachUntilFirstError for validating slices and maps with lot…
Browse files Browse the repository at this point in the history
…s of items
  • Loading branch information
Christian Theilemann committed Jan 20, 2020
1 parent 094faa1 commit b651690
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/ozzo-validation.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 84 additions & 0 deletions each_until_first_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2016 Qiang Xue. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package validation

import (
"errors"
"reflect"
"strconv"
)

// EachUntilFirstError is the same as Each but stops early once the first item with a validation error was encountered.
// Use this instead of Each for array's or maps that may potentially contain ten-thousands of erroneous items and
// you want to avoid returning ten-thousands of validation errors (for memory and cpu reasons).
func EachUntilFirstError(rules ...Rule) EachUntilFirstErrorRule {
return EachUntilFirstErrorRule{
rules: rules,
}
}

// EachUntilFirstErrorRule is the same as EachRule but stops early once the first item with a validation error was encountered.
// Use this instead of EachRule for array's or maps that may potentially contain ten-thousands of erroneous items and
// you want to avoid returning ten-thousands of validation errors (for memory and cpu reasons).
type EachUntilFirstErrorRule struct {
rules []Rule
limit int
}

// Validate loops through the given iterable and calls the Ozzo Validate() method for each value.
func (r EachUntilFirstErrorRule) Validate(value interface{}) error {
errs := Errors{}

v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Map:
for _, k := range v.MapKeys() {
val := r.getInterface(v.MapIndex(k))
if err := Validate(val, r.rules...); err != nil {
errs[r.getString(k)] = err
break
}
}
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
val := r.getInterface(v.Index(i))
if err := Validate(val, r.rules...); err != nil {
errs[strconv.Itoa(i)] = err
break
}
}
default:
return errors.New("must be an iterable (map, slice or array)")
}

if len(errs) > 0 {
return errs
}
return nil
}

func (r EachUntilFirstErrorRule) getInterface(value reflect.Value) interface{} {
switch value.Kind() {
case reflect.Ptr, reflect.Interface:
if value.IsNil() {
return nil
}
return value.Elem().Interface()
default:
return value.Interface()
}
}

func (r EachUntilFirstErrorRule) getString(value reflect.Value) string {
switch value.Kind() {
case reflect.Ptr, reflect.Interface:
if value.IsNil() {
return ""
}
return value.Elem().String()
default:
return value.String()
}
}
39 changes: 39 additions & 0 deletions each_until_first_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package validation

import (
"testing"
)

func TestEachUntilFirstError(t *testing.T) {
var a *int
var f = func(v string) string { return v }
var c0 chan int
c1 := make(chan int)

tests := []struct {
tag string
value interface{}
err string
}{
{"t1", nil, "must be an iterable (map, slice or array)"},
{"t2", map[string]string{}, ""},
{"t3", map[string]string{"key1": "value1", "key2": "value2"}, ""},
{"t4", map[string]string{"key1": "", "key2": "value2", "key3": ""}, "key1: cannot be blank."},
{"t5", map[string]map[string]string{"key1": {"key1.1": "value1"}, "key2": {"key2.1": "value1"}}, ""},
{"t6", map[string]map[string]string{"": nil}, ": cannot be blank."},
{"t7", map[interface{}]interface{}{}, ""},
{"t8", map[interface{}]interface{}{"key1": struct{ foo string }{"foo"}}, ""},
{"t9", map[interface{}]interface{}{nil: "", "": "", "key1": nil}, ": cannot be blank."},
{"t10", []string{"value1", "value2", "value3"}, ""},
{"t11", []string{"", "value2", ""}, "0: cannot be blank."},
{"t12", []interface{}{struct{ foo string }{"foo"}}, ""},
{"t13", []interface{}{nil, a}, "0: cannot be blank."},
{"t14", []interface{}{c0, c1, f}, "0: cannot be blank."},
}

for _, test := range tests {
r := EachUntilFirstError(Required)
err := r.Validate(test.value)
assertError(t, test.err, err, test.tag)
}
}

0 comments on commit b651690

Please sign in to comment.