validate/validate.go

313 lines
6.1 KiB
Go
Raw Normal View History

2018-10-16 20:18:39 +02:00
package validate
import (
"reflect"
"strconv"
"strings"
"sync"
)
2020-11-04 17:07:45 +01:00
type ValidateValuer interface {
ValidateValue() interface{}
}
var vvType = reflect.TypeOf((*ValidateValuer)(nil)).Elem()
2018-10-16 20:18:39 +02:00
// 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)
2019-01-23 14:11:04 +01:00
if idx := strings.IndexRune(name, ','); idx != -1 {
name = name[:idx]
}
2018-10-16 20:18:39 +02:00
}
2019-01-23 14:11:04 +01:00
2018-10-16 20:18:39 +02:00
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)
2020-04-24 13:01:25 +02:00
2020-11-04 17:07:45 +01:00
var valuer bool
if ft.Type.Implements(vvType) {
ft.Type = reflect.TypeOf(reflect.New(ft.Type).Interface().(ValidateValuer).ValidateValue())
valuer = true
}
kind := simplifyKind(ft.Type.Kind())
2020-04-24 13:01:25 +02:00
var ptr bool
if kind == reflect.Ptr {
kind = ft.Type.Elem().Kind()
ptr = true
}
kind = simplifyKind(kind)
2018-10-16 20:18:39 +02:00
tags := strings.Split(ft.Tag.Get(`validate`), `,`)
2020-11-04 17:07:45 +01:00
rules = append(rules, getTagFuncs(i, ft, kind, tags, ptr, valuer)...)
2018-10-16 20:18:39 +02:00
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
}
}
2020-11-04 17:07:45 +01:00
func getTagFuncs(i int, ft reflect.StructField, kind reflect.Kind, tags []string, ptr, valuer bool) []rule {
2018-10-16 20:18:39 +02:00
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]
}
2020-04-24 13:01:25 +02:00
kind := kind
ptr := ptr
if ptr && (tag == `optional` || strings.TrimPrefix(tag, `!`) == `required`) {
kind = reflect.Ptr
ptr = false
}
2020-11-04 17:07:45 +01:00
var f validateCheck
2018-10-16 20:18:39 +02:00
if tag == `optional` {
2020-11-04 17:07:45 +01:00
f = func(rv reflect.Value) ([]ValidationError, bool) {
2018-10-16 20:18:39 +02:00
check, _ := getTagFunc(`required`, ``, kind)
return nil, check(rv, nil)
2020-11-04 17:07:45 +01:00
}
} else {
var not bool
if strings.HasPrefix(tag, `!`) {
not = true
2018-10-16 20:18:39 +02:00
}
2020-11-04 17:07:45 +01:00
check, val := getTagFunc(strings.TrimPrefix(tag, `!`), value, kind)
2018-10-16 20:18:39 +02:00
2020-04-24 13:01:25 +02:00
f = func(rv reflect.Value) ([]ValidationError, bool) {
2020-11-04 17:07:45 +01:00
if check(rv, val) == !not {
return nil, true
2020-04-24 13:01:25 +02:00
}
2020-11-04 17:07:45 +01:00
return []ValidationError{{Field: []Field{{nil, &ft}}, Check: tag, Value: value}}, false
2020-04-24 13:01:25 +02:00
}
}
2020-11-04 17:07:45 +01:00
if ptr {
f = depointerFunc(f, ft, tag, value)
}
if valuer {
f = valuerFunc(f)
}
2018-10-16 20:18:39 +02:00
rules = append(rules, rule{i, f})
}
return rules
}
2020-11-04 17:07:45 +01:00
type validateCheck = func(rv reflect.Value) ([]ValidationError, bool)
func depointerFunc(f validateCheck, ft reflect.StructField, tag, value string) validateCheck {
return func(rv reflect.Value) ([]ValidationError, bool) {
if rv.IsNil() {
return []ValidationError{{Field: []Field{{nil, &ft}}, Check: tag, Value: value}}, false
}
return f(rv.Elem())
}
}
func valuerFunc(f validateCheck) validateCheck {
return func(rv reflect.Value) ([]ValidationError, bool) {
rv = reflect.ValueOf(rv.Interface().(ValidateValuer).ValidateValue())
return f(rv)
}
}
func getTagFunc(tag, value string, kind reflect.Kind) (ValidationFunc, interface{}) {
2018-10-16 20:18:39 +02:00
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
}