-
Notifications
You must be signed in to change notification settings - Fork 0
/
validator.go
161 lines (137 loc) · 4 KB
/
validator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package validate
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"go.uber.org/multierr"
)
// FieldNameFunc is a function which converts a given reflect.StructField to a
// string. The default will lookup json, yaml, and form field tags.
type FieldNameFunc func(reflect.StructField) string
// FieldJoinFunc joins a path slice with a given field. Both path and field may
// be empty values.
type FieldJoinFunc func(path []string, field string) string
// Validator validates Validatable objects.
type Validator struct {
fieldName FieldNameFunc
fieldJoin FieldJoinFunc
}
// New creates a new Validator.
func New() *Validator {
return &Validator{}
}
// Validate will validate the given object. Structs, maps, slices, and arrays
// will have each of their fields/items validated, effectively performing a
// deep-validation.
func (s *Validator) Validate(data interface{}) error {
if s.fieldName == nil {
s.fieldName = DefaultFieldName
}
if s.fieldJoin == nil {
s.fieldJoin = DefaultFieldJoin
}
return s.validate(nil, data)
}
// FieldNameFunc allows setting a custom FieldNameFunc method. It receives a
// reflect.StructField, and must return a string for the name of that field. If
// the returned string is empty, validation will not run against the field's
// value, or any nested data within.
func (s *Validator) FieldNameFunc(f FieldNameFunc) {
s.fieldName = f
}
// FieldJoinFunc allows setting a custom FieldJoinFunc method. It receives a
// string slice of parent fields, and a string of the field name the error is
// reported against. All parent paths, must be joined with the current.
func (s *Validator) FieldJoinFunc(f FieldJoinFunc) {
s.fieldJoin = f
}
func (s *Validator) validate(path []string, data interface{}) error {
var errs error
if data == nil {
return nil
}
d := reflect.ValueOf(data)
if d.Kind() == reflect.Ptr {
if d.IsNil() {
return nil
}
d = d.Elem()
}
if v, ok := data.(Validatable); ok {
verrs := v.Validate()
for _, err := range multierr.Errors(verrs) {
// Create a new Error for all errors returned by Validate function
// to correctly resolve field name, and also field path in relation
// to parent objects being validated.
newErr := &Error{}
e := &Error{}
if ok := errors.As(err, &e); ok {
field := e.Field
if field != "" && d.Kind() == reflect.Struct {
if sf, ok := d.Type().FieldByName(e.Field); ok {
field = s.fieldName(sf)
}
}
newErr.Field = s.fieldJoin(path, field)
newErr.Msg = e.Msg
newErr.Err = e.Err
} else {
newErr.Field = s.fieldJoin(path, "")
newErr.Err = err
}
errs = multierr.Append(errs, newErr)
}
}
switch d.Kind() { //nolint:exhaustive
case reflect.Slice, reflect.Array:
for i := 0; i < d.Len(); i++ {
v := d.Index(i)
err := s.validate(append(path, strconv.Itoa(i)), v.Interface())
errs = multierr.Append(errs, err)
}
case reflect.Map:
for _, k := range d.MapKeys() {
v := d.MapIndex(k)
err := s.validate(append(path, fmt.Sprintf("%v", k)), v.Interface())
errs = multierr.Append(errs, err)
}
case reflect.Struct:
for i := 0; i < d.NumField(); i++ {
v := d.Field(i)
fldName := s.fieldName(d.Type().Field(i))
if v.CanSet() && fldName != "" {
err := s.validate(append(path, fldName), v.Interface())
errs = multierr.Append(errs, err)
}
}
}
return errs
}
// DefaultFieldName is the default FieldNameFunc used by Validator.
//
// Uses json, yaml, and form field tags to lookup field name first.
func DefaultFieldName(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "" {
name = strings.SplitN(fld.Tag.Get("yaml"), ",", 2)[0]
}
if name == "" {
name = strings.SplitN(fld.Tag.Get("form"), ",", 2)[0]
}
if name == "-" {
return ""
}
if name == "" {
return fld.Name
}
return name
}
// DefaultFieldJoin is the default FieldJoinFunc used by Validator.
func DefaultFieldJoin(path []string, field string) string {
if field != "" {
path = append(path, field)
}
return strings.Join(path, ".")
}