commit
5778878a2a
6 changed files with 665 additions and 0 deletions
@ -0,0 +1,35 @@
|
||||
package validate |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
type testCase struct { |
||||
A string `validate:"required"` |
||||
B int `validate:"gt=9"` |
||||
C float64 `validate:"eq=10"` |
||||
D uint64 `validate:"required,lt=9"` |
||||
} |
||||
|
||||
var case1 = testCase{`abc`, 10, 10.0, 5} |
||||
var case2 = testCase{`abc`, 8, 10.0, 0} |
||||
|
||||
func BenchmarkValidateCorrect(b *testing.B) { |
||||
var errs []ValidationError |
||||
for i := 0; i < b.N; i++ { |
||||
errs = Validate(case1) |
||||
if len(errs) != 0 { |
||||
b.Fatal(`This case should pass but got`, errs) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func BenchmarkValidateIncorrect(b *testing.B) { |
||||
var errs []ValidationError |
||||
for i := 0; i < b.N; i++ { |
||||
errs = Validate(case2) |
||||
if len(errs) != 2 { |
||||
b.Fatal(`This case should get 2 errors but got`, len(errs)) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,41 @@
|
||||
package validate |
||||
|
||||
import ( |
||||
"reflect" |
||||
"regexp" |
||||
"strconv" |
||||
) |
||||
|
||||
func inputInt(kind reflect.Kind, value string) interface{} { |
||||
return int(inputSame(reflect.Int, value).(int64)) |
||||
} |
||||
|
||||
func inputRegexp(kind reflect.Kind, value string) interface{} { |
||||
return regexp.MustCompile(value) |
||||
} |
||||
|
||||
func inputSame(kind reflect.Kind, value string) interface{} { |
||||
var val interface{} |
||||
var err error |
||||
|
||||
switch kind { |
||||
case reflect.String: |
||||
val = value |
||||
case reflect.Int: |
||||
val, err = strconv.ParseInt(value, 10, 64) |
||||
case reflect.Uint: |
||||
val, err = strconv.ParseUint(value, 10, 64) |
||||
case reflect.Float64: |
||||
val, err = strconv.ParseFloat(value, 64) |
||||
case reflect.Bool: |
||||
val, err = strconv.ParseBool(value) |
||||
default: |
||||
panic(`Cannot pass value to checks on type ` + kind.String()) |
||||
} |
||||
|
||||
if err != nil { |
||||
panic(`Invalid value "` + value + `"`) |
||||
} |
||||
|
||||
return val |
||||
} |
@ -0,0 +1,142 @@
|
||||
package validate |
||||
|
||||
import ( |
||||
"reflect" |
||||
"regexp" |
||||
"strings" |
||||
) |
||||
|
||||
type listFunc func(reflect.Value, interface{}) bool |
||||
type listFuncInfo struct { |
||||
inputFunc func(reflect.Kind, string) interface{} |
||||
kinds _kinds |
||||
} |
||||
|
||||
type _kinds map[reflect.Kind]listFunc |
||||
|
||||
// nolint: dupl
|
||||
var funcs = map[string]listFuncInfo{ |
||||
`required`: {nil, _kinds{ |
||||
reflect.Ptr: func(rv reflect.Value, _ interface{}) bool { |
||||
return !rv.IsNil() |
||||
}, |
||||
reflect.Interface: func(rv reflect.Value, _ interface{}) bool { |
||||
return !rv.IsNil() |
||||
}, |
||||
reflect.Slice: func(rv reflect.Value, _ interface{}) bool { |
||||
return !rv.IsNil() && rv.Len() > 0 |
||||
}, |
||||
reflect.Map: func(rv reflect.Value, _ interface{}) bool { |
||||
return !rv.IsNil() && rv.Len() > 0 |
||||
}, |
||||
reflect.String: func(rv reflect.Value, _ interface{}) bool { |
||||
return rv.String() != `` |
||||
}, |
||||
reflect.Int: func(rv reflect.Value, _ interface{}) bool { |
||||
return rv.Int() != 0 |
||||
}, |
||||
reflect.Uint: func(rv reflect.Value, _ interface{}) bool { |
||||
return rv.Uint() != 0 |
||||
}, |
||||
reflect.Float64: func(rv reflect.Value, _ interface{}) bool { |
||||
return rv.Float() != 0 |
||||
}, |
||||
}}, |
||||
|
||||
// Strings
|
||||
`prefix`: {inputSame, _kinds{ |
||||
reflect.String: func(rv reflect.Value, val interface{}) bool { |
||||
return strings.HasPrefix(rv.String(), val.(string)) |
||||
}, |
||||
}}, |
||||
`suffix`: {inputSame, _kinds{ |
||||
reflect.String: func(rv reflect.Value, val interface{}) bool { |
||||
return strings.HasSuffix(rv.String(), val.(string)) |
||||
}, |
||||
}}, |
||||
`contains`: {inputSame, _kinds{ |
||||
reflect.String: func(rv reflect.Value, val interface{}) bool { |
||||
return strings.Contains(rv.String(), val.(string)) |
||||
}, |
||||
}}, |
||||
`regexp`: {inputRegexp, _kinds{ |
||||
reflect.String: func(rv reflect.Value, val interface{}) bool { |
||||
return val.(*regexp.Regexp).MatchString(rv.String()) |
||||
}, |
||||
}}, |
||||
|
||||
// Comparisons
|
||||
`eq`: {inputSame, _kinds{ |
||||
reflect.String: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.String() == val.(string) |
||||
}, |
||||
reflect.Int: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Int() == val.(int64) |
||||
}, |
||||
reflect.Uint: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Uint() == val.(uint64) |
||||
}, |
||||
reflect.Float64: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Float() == val.(float64) |
||||
}, |
||||
}}, |
||||
|
||||
// Integers
|
||||
`gt`: {inputSame, _kinds{ |
||||
reflect.Int: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Int() > val.(int64) |
||||
}, |
||||
reflect.Uint: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Uint() > val.(uint64) |
||||
}, |
||||
reflect.Float64: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Float() > val.(float64) |
||||
}, |
||||
}}, |
||||
`lt`: {inputSame, _kinds{ |
||||
reflect.Int: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Int() < val.(int64) |
||||
}, |
||||
reflect.Uint: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Uint() < val.(uint64) |
||||
}, |
||||
reflect.Float64: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Float() < val.(float64) |
||||
}, |
||||
}}, |
||||
|
||||
// Slices, maps & strings
|
||||
`len`: {inputInt, _kinds{ |
||||
reflect.Slice: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Len() == val.(int) |
||||
}, |
||||
reflect.Map: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Len() == val.(int) |
||||
}, |
||||
reflect.String: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Len() == val.(int) |
||||
}, |
||||
}}, |
||||
`min`: {inputInt, _kinds{ |
||||
reflect.Slice: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Len() >= val.(int) |
||||
}, |
||||
reflect.Map: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Len() >= val.(int) |
||||
}, |
||||
reflect.String: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Len() >= val.(int) |
||||
}, |
||||
}}, |
||||
`max`: {inputInt, _kinds{ |
||||
reflect.Slice: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Len() <= val.(int) |
||||
}, |
||||
reflect.Map: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Len() <= val.(int) |
||||
}, |
||||
reflect.String: func(rv reflect.Value, val interface{}) bool { |
||||
return rv.Len() <= val.(int) |
||||
}, |
||||
}}, |
||||
} |
@ -0,0 +1,118 @@
|
||||
package validate |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
func TestRuleRequired(t *testing.T) { |
||||
type s struct { |
||||
A *string `validate:"required"` |
||||
B []int `validate:"required"` |
||||
C []int `validate:"required"` |
||||
D string `validate:"required"` |
||||
E int `validate:"required"` |
||||
F uint `validate:"required"` |
||||
G float64 `validate:"required"` |
||||
H interface{} `validate:"required"` |
||||
I map[int]int `validate:"required"` |
||||
} |
||||
|
||||
str := `` |
||||
var pass = s{&str, make([]int, 1), make([]int, 1), ` `, -1, 1, 0.01, ``, map[int]int{0: 1}} |
||||
|
||||
var fail = s{nil, nil, make([]int, 0), ``, 0, 0, 0.000, nil, nil} |
||||
|
||||
check(t, pass, 0) |
||||
check(t, fail, 9) |
||||
} |
||||
|
||||
func TestRulePrefixSuffix(t *testing.T) { |
||||
type s struct { |
||||
A string `validate:"prefix=#"` |
||||
B string `validate:"suffix=@"` |
||||
} |
||||
|
||||
var pass = s{`#a`, `a@`} |
||||
|
||||
var fail = s{`a#`, `@a`} |
||||
|
||||
check(t, pass, 0) |
||||
check(t, fail, 2) |
||||
} |
||||
|
||||
func TestRuleContains(t *testing.T) { |
||||
type s struct { |
||||
A string `validate:"contains=%"` |
||||
} |
||||
|
||||
var pass1 = s{`a%`} |
||||
var pass2 = s{`%a`} |
||||
var pass3 = s{`%`} |
||||
var pass4 = s{`a%a`} |
||||
|
||||
var fail = s{`aa`} |
||||
|
||||
check(t, pass1, 0) |
||||
check(t, pass2, 0) |
||||
check(t, pass3, 0) |
||||
check(t, pass4, 0) |
||||
check(t, fail, 1) |
||||
} |
||||
|
||||
func TestRuleRegexp(t *testing.T) { |
||||
type s struct { |
||||
A string `validate:"regexp=^[0-9]$"` |
||||
} |
||||
|
||||
var pass1 = s{`0`} |
||||
var pass2 = s{`7`} |
||||
|
||||
var fail1 = s{`A`} |
||||
var fail2 = s{`11`} |
||||
|
||||
check(t, pass1, 0) |
||||
check(t, pass2, 0) |
||||
check(t, fail1, 1) |
||||
check(t, fail2, 1) |
||||
} |
||||
|
||||
func TestRuleEqGtLt(t *testing.T) { |
||||
type s struct { |
||||
A int `validate:"eq=3"` |
||||
B float64 `validate:"gt=1e5"` |
||||
C uint `validate:"lt=1"` |
||||
} |
||||
|
||||
var pass = s{3, 100001, 0} |
||||
|
||||
var fail1 = s{2, 1e5, 1} |
||||
var fail2 = s{4, 9999, 2} |
||||
|
||||
check(t, pass, 0) |
||||
check(t, fail1, 3) |
||||
check(t, fail2, 3) |
||||
} |
||||
|
||||
func TestLenMinMax(t *testing.T) { |
||||
type s struct { |
||||
A string `validate:"len=3"` |
||||
B []int `validate:"min=2"` |
||||
C map[int]string `validate:"max=1"` |
||||
} |
||||
|
||||
var pass = s{`abc`, []int{1, 2}, nil} |
||||
|
||||
var fail1 = s{`ab`, []int{1}, map[int]string{1: `a`, 2: `b`}} |
||||
var fail2 = s{`abcd`, nil, nil} |
||||
|
||||
check(t, pass, 0) |
||||
check(t, fail1, 3) |
||||
check(t, fail2, 2) |
||||
} |
||||
|
||||
func check(t *testing.T, c interface{}, errCount int) { |
||||
errs := Validate(c) |
||||
if len(errs) != errCount { |
||||
t.Errorf(`Case %T(%v) should get %d errors, but got %v`, c, c, errCount, errs) |
||||
} |
||||
} |
@ -0,0 +1,251 @@
|
||||
package validate |
||||
|
||||
import ( |
||||
"reflect" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
) |
||||
|
||||
// Validate validates a variable
|
||||
func Validate(v interface{}) []ValidationError { |
||||
rv := reflect.ValueOf(v) |
||||
|
||||
return validate(rv) |
||||
} |
||||
|
||||
// Field is always an array/slice index or struct field
|
||||
type Field struct { |
||||
Index *int |
||||
Field *reflect.StructField |
||||
} |
||||
|
||||
// Fields is a list of Field
|
||||
type Fields []Field |
||||
|
||||
// ToString converts a list of fields to a string using the given struct tag
|
||||
func (f Fields) ToString(tag string) string { |
||||
var field string |
||||
for k, v := range f { |
||||
if v.Index != nil { |
||||
field += `[` + strconv.Itoa(*v.Index) + `]` |
||||
continue |
||||
} |
||||
|
||||
if k != 0 { |
||||
field += `.` |
||||
} |
||||
|
||||
var name string |
||||
if tag != `` { |
||||
name = v.Field.Tag.Get(tag) |
||||
} |
||||
if name == `` { |
||||
name = v.Field.Name |
||||
} |
||||
|
||||
field += name |
||||
} |
||||
|
||||
return field |
||||
} |
||||
|
||||
func (f Fields) String() string { |
||||
return f.ToString(``) |
||||
} |
||||
|
||||
// ValidationError contains information about a failed validation
|
||||
type ValidationError struct { |
||||
Field Fields |
||||
Check string |
||||
Value string |
||||
} |
||||
|
||||
func (e ValidationError) String() string { |
||||
var val string |
||||
if e.Value != `` { |
||||
val = `=` + e.Value |
||||
} |
||||
return e.Field.String() + `: ` + e.Check + val |
||||
} |
||||
|
||||
func prependErrs(f Field, errs []ValidationError) []ValidationError { |
||||
for k := range errs { |
||||
fields := make([]Field, len(errs[k].Field)+1) |
||||
|
||||
fields[0] = f |
||||
for k, v := range errs[k].Field { |
||||
fields[k+1] = v |
||||
} |
||||
|
||||
errs[k].Field = fields |
||||
} |
||||
|
||||
return errs |
||||
} |
||||
|
||||
func validate(rv reflect.Value) []ValidationError { |
||||
for rv.Kind() == reflect.Ptr { |
||||
if rv.IsNil() { |
||||
return nil |
||||
} |
||||
|
||||
rv = rv.Elem() |
||||
} |
||||
|
||||
if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice { |
||||
var errs []ValidationError |
||||
for i := 0; i < rv.Len(); i++ { |
||||
newErrs := validate(rv.Index(i)) |
||||
|
||||
index := i |
||||
errs = append(errs, prependErrs(Field{&index, nil}, newErrs)...) |
||||
} |
||||
return errs |
||||
} |
||||
|
||||
if rv.Kind() != reflect.Struct { |
||||
return nil |
||||
} |
||||
|
||||
var errs []ValidationError |
||||
skip := -1 |
||||
for _, rule := range getCachedRules(rv.Type()) { |
||||
if skip == rule.index { |
||||
continue |
||||
} |
||||
|
||||
err, cont := rule.f(rv.Field(rule.index)) |
||||
errs = append(errs, err...) |
||||
|
||||
if !cont { |
||||
skip = rule.index |
||||
} |
||||
} |
||||
|
||||
return errs |
||||
} |
||||
|
||||
var cache = struct { |
||||
sync.Mutex |
||||
data map[reflect.Type][]rule |
||||
}{data: map[reflect.Type][]rule{}} |
||||
|
||||
func getCachedRules(rt reflect.Type) []rule { |
||||
cache.Lock() |
||||
defer cache.Unlock() |
||||
|
||||
rules, ok := cache.data[rt] |
||||
if !ok { |
||||
rules = getRules(rt) |
||||
|
||||
cache.data[rt] = rules |
||||
} |
||||
|
||||
return rules |
||||
} |
||||
|
||||
type rule struct { |
||||
index int |
||||
f func(reflect.Value) ([]ValidationError, bool) |
||||
} |
||||
|
||||
func getRules(rt reflect.Type) []rule { |
||||
var rules []rule |
||||
|
||||
for i := 0; i < rt.NumField(); i++ { |
||||
ft := rt.Field(i) |
||||
kind := simplifyKind(ft.Type.Kind()) |
||||
|
||||
tags := strings.Split(ft.Tag.Get(`validate`), `,`) |
||||
rules = append(rules, getTagFuncs(i, ft, kind, tags)...) |
||||
|
||||
// TODO: Add validator interface
|
||||
|
||||
switch kind { |
||||
case reflect.Slice, reflect.Struct, reflect.Interface: |
||||
rules = append(rules, rule{i, nest(ft)}) |
||||
} |
||||
} |
||||
|
||||
return rules |
||||
} |
||||
|
||||
func nest(ft reflect.StructField) func(reflect.Value) ([]ValidationError, bool) { |
||||
return func(rv reflect.Value) ([]ValidationError, bool) { |
||||
errs := validate(rv) |
||||
if errs != nil { |
||||
return prependErrs(Field{nil, &ft}, errs), false |
||||
} |
||||
return nil, true |
||||
} |
||||
} |
||||
|
||||
func getTagFuncs(i int, ft reflect.StructField, kind reflect.Kind, tags []string) []rule { |
||||
var rules []rule |
||||
for _, v := range tags { |
||||
if v == `` { |
||||
continue |
||||
} |
||||
parts := strings.SplitN(v, `=`, 2) |
||||
|
||||
tag, value := parts[0], `` |
||||
if len(parts) > 1 { |
||||
value = parts[1] |
||||
} |
||||
|
||||
if tag == `optional` { |
||||
rules = append(rules, rule{i, func(rv reflect.Value) ([]ValidationError, bool) { |
||||
check, _ := getTagFunc(`required`, ``, kind) |
||||
return nil, check(rv, nil) |
||||
}}) |
||||
continue |
||||
} |
||||
|
||||
check, val := getTagFunc(tag, value, kind) |
||||
|
||||
f := func(rv reflect.Value) ([]ValidationError, bool) { |
||||
if check(rv, val) { |
||||
return nil, true |
||||
} |
||||
|
||||
return []ValidationError{{Field: []Field{{nil, &ft}}, Check: tag, Value: value}}, false |
||||
} |
||||
|
||||
rules = append(rules, rule{i, f}) |
||||
} |
||||
|
||||
return rules |
||||
} |
||||
|
||||
func getTagFunc(tag, value string, kind reflect.Kind) (listFunc, interface{}) { |
||||
tagInfo, ok := funcs[tag] |
||||
if !ok { |
||||
panic(`Unknown validation ` + tag) |
||||
} |
||||
check, ok := tagInfo.kinds[kind] |
||||
if !ok { |
||||
panic(`Validation ` + tag + ` does not support ` + kind.String()) |
||||
} |
||||
|
||||
var val interface{} |
||||
if value != `` && tagInfo.inputFunc != nil { |
||||
val = tagInfo.inputFunc(kind, value) |
||||
} |
||||
|
||||
return check, val |
||||
} |
||||
|
||||
func simplifyKind(kind reflect.Kind) reflect.Kind { |
||||
switch kind { |
||||
case reflect.Array: |
||||
return reflect.Slice |
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
return reflect.Int |
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||
return reflect.Uint |
||||
case reflect.Float32: |
||||
return reflect.Float64 |
||||
} |
||||
return kind |
||||
} |
@ -0,0 +1,78 @@
|
||||
package validate |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
func TestOptionalMultiple(t *testing.T) { |
||||
type s struct { |
||||
A string `validate:"optional,eq=a"` |
||||
B int `validate:"gt=3,lt=20"` |
||||
} |
||||
|
||||
var pass1 = s{``, 4} |
||||
var pass2 = s{`a`, 19} |
||||
|
||||
var fail = s{`b`, 3} |
||||
|
||||
check(t, pass1, 0) |
||||
check(t, pass2, 0) |
||||
check(t, fail, 2) |
||||
} |
||||
|
||||
func TestNesting(t *testing.T) { |
||||
type sa struct { |
||||
AA string `validate:"required"` |
||||
} |
||||
|
||||
type sb struct { |
||||
BA int `validate:"gt=10"` |
||||
} |
||||
|
||||
type s struct { |
||||
A []sa `validate:"required"` |
||||
B sb |
||||
} |
||||
|
||||
var pass = s{[]sa{{`abc`}}, sb{12}} |
||||
|
||||
var fail1 = s{nil, sb{12}} |
||||
var fail2 = s{[]sa{{``}}, sb{12}} |
||||
var fail3 = s{[]sa{{``}}, sb{9}} |
||||
|
||||
check(t, pass, 0) |
||||
check(t, fail1, 1) |
||||
check(t, fail2, 1) |
||||
check(t, fail3, 2) |
||||
} |
||||
|
||||
func TestValidationErrorField(t *testing.T) { |
||||
type sc struct { |
||||
D int `validate:"eq=1"` |
||||
} |
||||
type sb struct { |
||||
C []sc |
||||
} |
||||
type sa struct { |
||||
B sb |
||||
} |
||||
type s struct { |
||||
A sa |
||||
} |
||||
|
||||
errs := Validate(s{ |
||||
A: sa{ |
||||
B: sb{ |
||||
C: []sc{ |
||||
{D: 0}, |
||||
{D: 1}, |
||||
{D: 2}, |
||||
}, |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
if errs[0].Field.String() != `A.B.C[0].D` || errs[1].Field.String() != `A.B.C[2].D` { |
||||
t.Fatal(`Expected errors to be A.B.C[0].D and A.B.C[2].D; got`, errs[0].Field, `and`, errs[1].Field) |
||||
} |
||||
} |
Loading…
Reference in new issue