forked from Fuyu/validate
256 lines
4.8 KiB
Go
256 lines
4.8 KiB
Go
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 idx := strings.IndexRune(name, ','); idx != -1 {
|
|
name = name[:idx]
|
|
}
|
|
}
|
|
|
|
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) (ValidationFunc, 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
|
|
}
|