-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidate.go
More file actions
227 lines (198 loc) · 5.48 KB
/
Copy pathvalidate.go
File metadata and controls
227 lines (198 loc) · 5.48 KB
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
// Package govalidate provides struct validation using struct tags.
//
// # Quick Start
//
// type User struct {
// Name string `validate:"required,min=3,max=100"`
// Email string `validate:"required,email"`
// Age int `validate:"gte=0,lte=150"`
// }
//
// errs := govalidate.Validate(User{Name: "Jo", Email: "bad"})
// if errs.HasErrors() {
// for _, e := range errs.Errors {
// fmt.Println(e)
// }
// }
package govalidate
import (
"fmt"
"reflect"
"strings"
"sync"
)
const tagName = "validate"
// customRules holds user-registered custom rules.
var (
customRules = make(map[string]RuleFunc)
customRulesMu sync.RWMutex
)
// RegisterRule adds a custom validation rule.
//
// govalidate.RegisterRule("phone", func(v reflect.Value, param string) string {
// if v.Kind() != reflect.String { return "" }
// if !phoneRegex.MatchString(v.String()) {
// return "must be a valid phone number"
// }
// return ""
// })
func RegisterRule(name string, fn RuleFunc) {
customRulesMu.Lock()
defer customRulesMu.Unlock()
customRules[name] = fn
}
// Validate validates a struct using its `validate` tags.
// Returns ValidationError containing all field errors.
func Validate(v any) ValidationError {
return validateValue(reflect.ValueOf(v), "")
}
// validateValue recursively validates a value.
func validateValue(v reflect.Value, prefix string) ValidationError {
ve := ValidationError{}
// Handle pointer
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return ve
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return ve
}
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldValue := v.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
// Get field name (use json tag if available)
fieldName := getFieldName(field)
if prefix != "" {
fieldName = prefix + "." + fieldName
}
// Get validate tag
tag := field.Tag.Get(tagName)
if tag == "" || tag == "-" {
// Still recurse into nested structs
if fieldValue.Kind() == reflect.Struct && field.Type != reflect.TypeOf(struct{}{}) {
nested := validateValue(fieldValue, fieldName)
ve.Errors = append(ve.Errors, nested.Errors...)
}
continue
}
// Parse and apply rules
rules := parseTag(tag)
for _, rule := range rules {
msg := applyRule(rule.name, fieldValue, rule.param)
if msg != "" {
ve.Errors = append(ve.Errors, FieldError{
Field: fieldName,
Rule: rule.name,
Message: msg,
Value: safeValue(fieldValue),
})
}
}
// Recurse into nested structs
actual := fieldValue
if actual.Kind() == reflect.Ptr && !actual.IsNil() {
actual = actual.Elem()
}
if actual.Kind() == reflect.Struct {
nested := validateValue(actual, fieldName)
ve.Errors = append(ve.Errors, nested.Errors...)
}
// Validate slices of structs
if fieldValue.Kind() == reflect.Slice {
for j := 0; j < fieldValue.Len(); j++ {
item := fieldValue.Index(j)
if item.Kind() == reflect.Ptr && !item.IsNil() {
item = item.Elem()
}
if item.Kind() == reflect.Struct {
itemPrefix := fmt.Sprintf("%s[%d]", fieldName, j)
nested := validateValue(item, itemPrefix)
ve.Errors = append(ve.Errors, nested.Errors...)
}
}
}
}
return ve
}
// ─── Tag Parsing ──────────────────────────────────────────────────
type parsedRule struct {
name string
param string
}
// parseTag splits "required,min=3,max=100" into individual rules.
func parseTag(tag string) []parsedRule {
parts := strings.Split(tag, ",")
rules := make([]parsedRule, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
name, param, _ := strings.Cut(part, "=")
rules = append(rules, parsedRule{
name: strings.TrimSpace(name),
param: strings.TrimSpace(param),
})
}
return rules
}
// applyRule finds and executes a validation rule.
func applyRule(name string, value reflect.Value, param string) string {
// Check built-in rules first
if fn, ok := builtinRules[name]; ok {
return fn(value, param)
}
// Check custom rules
customRulesMu.RLock()
fn, ok := customRules[name]
customRulesMu.RUnlock()
if ok {
return fn(value, param)
}
return fmt.Sprintf("unknown validation rule: %s", name)
}
// ─── Helpers ──────────────────────────────────────────────────────
// getFieldName returns the field name, preferring the json tag.
func getFieldName(field reflect.StructField) string {
jsonTag := field.Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
name, _, _ := strings.Cut(jsonTag, ",")
if name != "" {
return name
}
}
return field.Name
}
// safeValue extracts a safe-to-log value from a reflect.Value.
func safeValue(v reflect.Value) any {
if !v.IsValid() {
return nil
}
switch v.Kind() {
case reflect.String:
s := v.String()
if len(s) > 100 {
return s[:100] + "..."
}
return s
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint()
case reflect.Float32, reflect.Float64:
return v.Float()
case reflect.Bool:
return v.Bool()
case reflect.Slice, reflect.Array:
return fmt.Sprintf("[%d items]", v.Len())
default:
return fmt.Sprintf("%v", v.Interface())
}
}