From f675819c38683071f832d7266fc651e55794e033 Mon Sep 17 00:00:00 2001 From: Christian Theilemann Date: Mon, 20 Jan 2020 13:03:22 -0800 Subject: [PATCH] introduce EachUntilFirstError for validating slices and maps with lots of items --- each_until_first_error.go | 84 ++++++++++++++++++++++++++++++++++ each_until_first_error_test.go | 39 ++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 each_until_first_error.go create mode 100644 each_until_first_error_test.go diff --git a/each_until_first_error.go b/each_until_first_error.go new file mode 100644 index 0000000..a00b339 --- /dev/null +++ b/each_until_first_error.go @@ -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() + } +} diff --git a/each_until_first_error_test.go b/each_until_first_error_test.go new file mode 100644 index 0000000..4a17803 --- /dev/null +++ b/each_until_first_error_test.go @@ -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) + } +}