Initial commit

This commit is contained in:
Nise Void 2018-10-16 20:18:39 +02:00
commit 5778878a2a
Signed by: NiseVoid
GPG Key ID: FBA14AC83EA602F3
6 changed files with 665 additions and 0 deletions

35
bench_test.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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)
}
}