validate/validate.go
2020-04-24 13:01:25 +02:00

286 lines
5.4 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 := ft.Type.Kind()
var ptr bool
if kind == reflect.Ptr {
kind = ft.Type.Elem().Kind()
ptr = true
}
kind = simplifyKind(kind)
tags := strings.Split(ft.Tag.Get(`validate`), `,`)
rules = append(rules, getTagFuncs(i, ft, kind, tags, ptr)...)
// 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, ptr bool) []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]
}
kind := kind
ptr := ptr
if ptr && (tag == `optional` || strings.TrimPrefix(tag, `!`) == `required`) {
kind = reflect.Ptr
ptr = false
}
if tag == `optional` {
rules = append(rules, rule{i, func(rv reflect.Value) ([]ValidationError, bool) {
check, _ := getTagFunc(`required`, ``, kind)
return nil, check(rv, nil)
}})
continue
}
var not bool
if strings.HasPrefix(tag, `!`) {
not = true
}
check, val := getTagFunc(strings.TrimPrefix(tag, `!`), value, kind)
f := func(rv reflect.Value) ([]ValidationError, bool) {
if check(rv, val) == !not {
return nil, true
}
return []ValidationError{{Field: []Field{{nil, &ft}}, Check: tag, Value: value}}, false
}
if ptr {
oldF := f
f = func(rv reflect.Value) ([]ValidationError, bool) {
if rv.IsNil() {
return []ValidationError{{Field: []Field{{nil, &ft}}, Check: tag, Value: value}}, false
}
return oldF(rv.Elem())
}
}
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
}