-
Notifications
You must be signed in to change notification settings - Fork 20
/
validator.go
165 lines (136 loc) · 4.89 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
162
163
164
165
package utils
import (
"fmt"
"io"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
"github.com/nyaruka/gocommon/jsonx"
)
// our system validator, it can be shared across threads
var valx = validator.New()
// ErrorMessageFunc is the type for a function that can convert a field error to user friendly message
type ErrorMessageFunc func(validator.FieldError) string
var messageFuncs = map[string]ErrorMessageFunc{
"required": func(e validator.FieldError) string { return "is required" },
"email": func(e validator.FieldError) string { return "is not a valid email address" },
"uuid": func(e validator.FieldError) string { return "must be a valid UUID" },
"uuid4": func(e validator.FieldError) string { return "must be a valid UUID4" },
"url": func(e validator.FieldError) string { return "is not a valid URL" },
"min": func(e validator.FieldError) string {
if e.Kind() == reflect.Slice {
return fmt.Sprintf("must have a minimum of %s items", e.Param())
}
return fmt.Sprintf("must be greater than or equal to %s", e.Param())
},
"max": func(e validator.FieldError) string {
if e.Kind() == reflect.Slice {
return fmt.Sprintf("must have a maximum of %s items", e.Param())
}
return fmt.Sprintf("must be less than or equal to %s", e.Param())
},
"startswith": func(e validator.FieldError) string { return fmt.Sprintf("must start with '%s'", e.Param()) },
"mutually_exclusive": func(e validator.FieldError) string {
return fmt.Sprintf("is mutually exclusive with '%s'", e.Param())
},
}
func init() {
// use JSON tags as field names in validation error messages
valx.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "" {
return "-"
}
return name
})
RegisterValidatorAlias("http_method", "eq=GET|eq=HEAD|eq=POST|eq=PUT|eq=PATCH|eq=DELETE", func(validator.FieldError) string {
return "is not a valid HTTP method"
})
}
// RegisterValidatorTag registers a tag
func RegisterValidatorTag(tag string, fn validator.Func, message ErrorMessageFunc) {
valx.RegisterValidation(tag, fn)
messageFuncs[tag] = message
}
// RegisterValidatorAlias registers a tag alias
func RegisterValidatorAlias(alias, tags string, message ErrorMessageFunc) {
valx.RegisterAlias(alias, tags)
messageFuncs[alias] = message
}
// RegisterStructValidator registers a struct level validator
func RegisterStructValidator(fn validator.StructLevelFunc, types ...any) {
valx.RegisterStructValidation(fn, types...)
}
// ValidationErrors combines multiple validation errors as a single error
type ValidationErrors []error
// Error returns a string representation of these validation errors
func (e ValidationErrors) Error() string {
errs := make([]string, len(e))
for i := range e {
errs[i] = e[i].Error()
}
return strings.Join(errs, ", ")
}
// Validate will run validation on the given object and return a set of field specific errors in the format:
// field <fieldname> <tag specific message>
//
// For example: "field 'flows' is required"
func Validate(obj any) error {
var err error
// gets the value stored in the interface var, and if it's a pointer, dereferences it
v := reflect.Indirect(reflect.ValueOf(obj))
if v.Type().Kind() == reflect.Slice {
err = valx.Var(obj, `required,dive`)
} else {
err = valx.Struct(obj)
}
if err == nil {
return nil
}
validationErrs, isValidationErr := err.(validator.ValidationErrors)
if !isValidationErr {
return err
}
newErrors := make([]error, len(validationErrs))
for i, fieldErr := range validationErrs {
location := fieldErr.Namespace()
if strings.HasSuffix(location, ".-") {
location = fieldErr.StructNamespace()
}
// the first part of the namespace is always the struct name so we remove it
parts := strings.Split(location, ".")[1:]
// and ignore any parts called - as these come from composition
newParts := make([]string, 0)
for _, part := range parts {
if part != "-" {
newParts = append(newParts, part)
}
}
location = strings.Join(newParts, ".")
// generate a more user friendly description of the problem
var problem string
messageFunc := messageFuncs[fieldErr.Tag()]
if messageFunc != nil {
problem = messageFunc(fieldErr)
} else {
problem = fmt.Sprintf("failed tag '%s'", fieldErr.Tag())
}
newErrors[i] = fmt.Errorf("field '%s' %s", location, problem)
}
return ValidationErrors(newErrors)
}
// UnmarshalAndValidate is a convenience function to unmarshal an object and validate it
func UnmarshalAndValidate(data []byte, obj any) error {
err := jsonx.Unmarshal(data, obj)
if err != nil {
return err
}
return Validate(obj)
}
// UnmarshalAndValidateWithLimit unmarshals a struct with a limit on how many bytes can be read from the given reader
func UnmarshalAndValidateWithLimit(reader io.ReadCloser, s any, limit int64) error {
if err := jsonx.UnmarshalWithLimit(reader, s, limit); err != nil {
return err
}
return Validate(s)
}