Initial commit
This commit is contained in:
commit
5778878a2a
35
bench_test.go
Normal file
35
bench_test.go
Normal file
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
41
input.go
Normal file
41
input.go
Normal file
@ -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
|
||||
}
|
142
rules.go
Normal file
142
rules.go
Normal file
@ -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)
|
||||
},
|
||||
}},
|
||||
}
|
118
rules_test.go
Normal file
118
rules_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
251
validate.go
Normal file
251
validate.go
Normal file
@ -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
|
||||
}
|
78
validate_test.go
Normal file
78
validate_test.go
Normal file
@ -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
Block a user