-
Notifications
You must be signed in to change notification settings - Fork 126
Expand file tree
/
Copy pathstruct_rules.go
More file actions
577 lines (511 loc) · 18.2 KB
/
Copy pathstruct_rules.go
File metadata and controls
577 lines (511 loc) · 18.2 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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
package validate
import (
"fmt"
"reflect"
"strings"
"github.com/gookit/goutil/reflects"
"github.com/gookit/validate/v2/internal/reflectx"
)
/*************************************************************
* Static rule template (P3b): build once per STATIC type and
* clone into each Validation, avoiding the per-value tag walk.
*************************************************************/
// ruleTemplate is the immutable, value-independent rule-collection snapshot of
// a STATIC struct type. It is produced once (lazily) by running the existing
// parseRulesFromTag against a fresh zero-value instance, then captured here.
//
// Because a STATIC type's rule set does not depend on any concrete value (it
// has no ptr-to-struct, slice-of-struct or map-of-struct fields), the snapshot
// taken from a zero value is byte-for-byte identical to what any value would
// produce. The snapshot is read-only after build and shared across goroutines.
type ruleTemplate struct {
rules []*Rule
filterRules []*FilterRule
optionals map[string]int8
defValues map[string]any
fieldNames map[string]int8
// translation tables (relative to a fresh Translator):
labelMap map[string]string // trans.labelMap
fieldMap map[string]string // trans.fieldMap (output names)
messages map[string]string // ONLY custom messages added during collection
}
// computeIsStatic reports whether rt's rule set is value-independent.
//
// A struct type is DYNAMIC iff anywhere in its field graph (recursing ONLY into
// non-pointer struct fields) there is a field that is:
// - a pointer to a struct (non time.Time), OR
// - a slice/array whose element is a struct (non time.Time), OR
// - a map whose value is a struct (non time.Time).
//
// Otherwise it is STATIC. Recursion only descends through non-pointer struct
// fields; Go forbids a non-pointer struct from (transitively) containing itself
// without a pointer, so this terminates. Any cycle necessarily goes through a
// pointer-to-struct, which is caught by the first rule above (DYNAMIC) before
// recursing. The ancestors guard is a defensive backstop.
func computeIsStatic(rt reflect.Type) bool {
var scan func(t reflect.Type, ancestors map[reflect.Type]bool) bool
scan = func(t reflect.Type, ancestors map[reflect.Type]bool) bool {
for i := 0; i < t.NumField(); i++ {
sf := t.Field(i)
name := sf.Name
// mirror parseRulesFromTag's unexported-field skip.
if name[0] >= 'a' && name[0] <= 'z' {
if !gOpt.ValidatePrivateFields {
continue
}
}
st := sf.Type
switch st.Kind() {
case reflect.Ptr:
// ptr-to-struct (non time.Time): nil vs non-nil changes whether
// sub-rules are collected -> DYNAMIC.
et := removeTypePtr(st)
if et.Kind() == reflect.Struct && et != timeType {
return false
}
case reflect.Struct:
if st == timeType {
continue
}
if ancestors[st] {
// only reachable via a non-ptr struct cycle, which Go
// disallows; treat defensively as dynamic.
return false
}
ancestors[st] = true
if !scan(st, ancestors) {
return false
}
delete(ancestors, st)
case reflect.Array, reflect.Slice:
et := removeTypePtr(st.Elem())
if et.Kind() == reflect.Struct && et != timeType {
return false
}
case reflect.Map:
et := removeTypePtr(st.Elem())
if et.Kind() == reflect.Struct && et != timeType {
return false
}
default:
// leaf field: no effect on static-ness.
}
}
return true
}
return scan(rt, map[reflect.Type]bool{rt: true})
}
// staticTemplate returns the cached rule template for a STATIC type, building
// it once via sync.Once. Safe for concurrent callers (multiple goroutines
// validating the same type).
func (m *typeMeta) staticTemplate() *ruleTemplate {
m.tplOnce.Do(func() {
m.tpl = buildRuleTemplate(m.Type)
})
return m.tpl
}
// buildRuleTemplate constructs the immutable rule snapshot for a STATIC struct
// type by reusing the existing parseRulesFromTag over a fresh zero-value
// instance. This guarantees the snapshot equals the live result byte-for-byte
// (same parsing code path), while only paying the cost once per type.
func buildRuleTemplate(rt reflect.Type) *ruleTemplate {
// temporary zero-value StructData + empty Validation to run the real parser.
zero := reflect.New(rt).Elem()
td := &StructData{
src: zero.Interface(),
value: zero,
valueTyp: rt,
ValidateTag: gOpt.ValidateTag,
FilterTag: gOpt.FilterTag,
fieldNames: make(map[string]int8),
}
tv := newEmpty()
tv.data = td
td.parseRulesFromTag(tv)
tpl := &ruleTemplate{
rules: tv.rules,
filterRules: tv.filterRules,
optionals: tv.optionals,
defValues: tv.defValues,
fieldNames: td.fieldNames,
labelMap: tv.trans.labelMap,
fieldMap: tv.trans.fieldMap,
}
// keep only custom messages (those differing from the builtin defaults),
// matching the dimension captured by the golden regression snapshot.
if len(tv.trans.messages) > 0 {
custom := make(map[string]string)
for k, val := range tv.trans.messages {
if base, ok := builtinMessages[k]; !ok || base != val {
custom[k] = val
}
}
if len(custom) > 0 {
tpl.messages = custom
}
}
// P3a: pre-convert each rule's string args to the validator-signature types
// once per STATIC type. This moves the per-validate convertArgsType cost to
// build time. tv reuses the same validatorMeta lookup the runtime path uses.
preConvertTemplateArgs(tpl.rules, tv)
return tpl
}
// preConvertTemplateArgs walks the static template rules and, for each rule
// backed by a BUILTIN validator, converts its string args to the validator's
// signature types via convertRuleArgs. On success the rule is marked argsReady
// so valueValidate skips the runtime conversion; on failure (or for
// non-builtin / unknown validators) the args are left untouched for the runtime
// path to handle. Only builtin validators are pre-converted so a later
// AddValidator override (different signature) can never use a stale typed arg.
func preConvertTemplateArgs(rules []*Rule, tv *Validation) {
for _, r := range rules {
// resolve funcMeta exactly as the runtime does (valueValidate uses the
// rule's realName as the validator name; template rules have no
// checkFuncMeta, so this falls through to validatorMeta lookup).
name := r.realName
fm := r.checkFuncMeta
if fm == nil {
fm = tv.validatorMeta(name)
}
// only pre-convert builtin validators (design §4.6).
if fm == nil || !fm.builtin {
continue
}
// compute addNum the same way valueValidate does: +1 for the value arg,
// +1 more when the validator's first arg is DataFace.
ft := fm.fv.Type()
addNum := 1
if ft.In(0) == dataFaceType {
addNum++
}
// convertRuleArgs converts in place; only mark argsReady on success so a
// failed conversion stays string and is retried (and reported) at runtime.
if err := convertRuleArgs(fm, "", r.arguments, addNum); err == nil {
r.argsReady = true
}
}
}
// instantiateStatic clones the cached STATIC template into the real Validation
// v and StructData d, producing a rule set identical to what parseRulesFromTag
// would have built for this value — but with zero reflection over the value.
func (d *StructData) instantiateStatic(v *Validation) {
if d.ValidateTag == "" {
d.ValidateTag = gOpt.ValidateTag
}
if d.FilterTag == "" {
d.FilterTag = gOpt.FilterTag
}
tpl := d.meta.staticTemplate()
// --- rules: clone each rule with its OWN args slice ---
// convertArgsType (validating.go) mutates r.arguments in place at validate
// time (string->typed). Sharing the template's args slice would corrupt the
// template / race across instances, so each instance gets a fresh copy.
if len(tpl.rules) > 0 {
// reuse this instance's OWN backing array when it is large enough (pooled
// reuse: resetForReuse already did v.rules[:0]); otherwise allocate one
// sized to the template. Either way it stays an instance-owned array (never
// the shared template's), so Reset/复用 + AddRule re-append never overwrites
// the template backing array.
if cap(v.rules) < len(tpl.rules) {
v.rules = make([]*Rule, 0, len(tpl.rules))
} else {
v.rules = v.rules[:0]
}
for _, tr := range tpl.rules {
if tr.argsReady {
// argsReady 模板规则在校验期完全只读(valueValidate 跳过 convertArgsType,
// 全仓库无任何校验期对 Rule 字段的写入),直接共享模板 *Rule 指针,免每实例
// cloneRule 分配;跨实例/跨 goroutine 共享安全。
v.rules = append(v.rules, tr)
} else {
// 非 argsReady 规则运行期仍会原地转换 args,每实例必须独立拷贝。
v.rules = append(v.rules, cloneRule(tr))
}
}
}
// --- filter rules: clone (filters slice + filterArgs map copied) ---
if len(tpl.filterRules) > 0 {
// same instance-owned-array reuse as v.rules above.
if cap(v.filterRules) < len(tpl.filterRules) {
v.filterRules = make([]*FilterRule, 0, len(tpl.filterRules))
} else {
v.filterRules = v.filterRules[:0]
}
for _, tfr := range tpl.filterRules {
v.filterRules = append(v.filterRules, cloneFilterRule(tfr))
}
}
// --- optionals ---
if len(tpl.optionals) > 0 {
v.ensureOptionals() // lazy
for k, val := range tpl.optionals {
v.optionals[k] = val
}
}
// --- default values ---
for k, val := range tpl.defValues {
v.SetDefValue(k, val)
}
// --- field names (TryGet/Set rely on these) ---
for k, val := range tpl.fieldNames {
d.fieldNames[k] = val
}
// --- translation tables: replay via the public helpers ---
for field, label := range tpl.labelMap {
v.trans.addLabelName(field, label)
}
if len(tpl.fieldMap) > 0 {
v.trans.AddFieldMap(tpl.fieldMap)
}
for key, msg := range tpl.messages {
v.trans.AddMessage(key, msg)
}
}
// cloneRule makes a shallow copy of an immutable template Rule.
//
// For an argsReady rule (P3a pre-converted), its args are already typed AND the
// runtime no longer mutates them (valueValidate skips convertArgsType), so the
// template's args slice is immutable and can be SHARED directly — saving a
// per-instance copy. For a non-argsReady rule the runtime still converts args in
// place, so each instance needs its own copy. nil args stay nil in both cases.
func cloneRule(tr *Rule) *Rule {
r := *tr // shallow copy of all scalar/ref fields
if tr.arguments != nil && !tr.argsReady {
args := make([]any, len(tr.arguments))
copy(args, tr.arguments)
r.arguments = args
} // else: argsReady (share immutable template args) OR nil (keep nil)
return &r
}
// cloneFilterRule deep-copies a template FilterRule's mutable-shaped fields
// (filters slice + filterArgs map) so per-instance use never touches the
// shared template.
func cloneFilterRule(tfr *FilterRule) *FilterRule {
fr := &FilterRule{}
if tfr.fields != nil {
fr.fields = make([]string, len(tfr.fields))
copy(fr.fields, tfr.fields)
}
if tfr.filters != nil {
fr.filters = make([]string, len(tfr.filters))
copy(fr.filters, tfr.filters)
}
fr.filterArgs = make(map[int]string, len(tfr.filterArgs))
for i, a := range tfr.filterArgs {
fr.filterArgs[i] = a
}
return fr
}
// parse and collect rules from struct tags.
func (d *StructData) parseRulesFromTag(v *Validation) {
if d.ValidateTag == "" {
d.ValidateTag = gOpt.ValidateTag
}
if d.FilterTag == "" {
d.FilterTag = gOpt.FilterTag
}
fOutMap := make(map[string]string)
var recursiveFunc func(vv reflect.Value, vt reflect.Type, preStrName string, parentIsAnonymous bool)
vv := d.value
vt := d.valueTyp
// preStrName - the parent field name.
recursiveFunc = func(vv reflect.Value, vt reflect.Type, parentFName string, parentIsAnonymous bool) {
for i := 0; i < vt.NumField(); i++ {
fv := vt.Field(i)
// skip don't exported field
name := fv.Name
if name[0] >= 'a' && name[0] <= 'z' {
if !gOpt.ValidatePrivateFields {
continue
}
}
if parentFName == "" {
d.fieldNames[name] = fieldAtTopStruct
} else {
name = parentFName + "." + name
if parentIsAnonymous {
d.fieldNames[name] = fieldAtAnonymous
} else {
d.fieldNames[name] = fieldAtSubStruct
}
}
// validate rule. use Lookup to distinguish "no tag" from "empty tag":
// an empty `validate:""` still marks the field for sub-struct cascade.
vRule, hasVRuleTag := fv.Tag.Lookup(d.ValidateTag)
if vRule != "" {
v.StringRule(name, vRule)
}
// filter rule
fRule := fv.Tag.Get(d.FilterTag)
if fRule != "" {
v.FilterRule(name, fRule)
}
// load field output name by FieldTag. eg: `json:"user_name"`
outName := ""
if gOpt.FieldTag != "" {
outName = fv.Tag.Get(gOpt.FieldTag)
outName = strings.SplitN(outName, ",", 2)[0]
}
// add pre field display name to fName
if outName != "" {
if parentFName != "" {
if pOutName, ok := fOutMap[parentFName]; ok {
outName = pOutName + "." + outName
}
}
fOutMap[name] = outName
}
// load field translate name
// preferred to use label tag name. eg: `label:"display name"`
// and then use field output name. eg: `json:"user_name"`
if gOpt.LabelTag != "" {
v.trans.addLabelName(name, fv.Tag.Get(gOpt.LabelTag))
}
// load custom error messages.
// eg: `message:"required:name is required|minLen:name min len is %d"`
if gOpt.MessageTag != "" {
errMsg := fv.Tag.Get(gOpt.MessageTag)
if errMsg != "" {
d.loadMessagesFromTag(v.trans, name, vRule, errMsg)
}
}
ft := removeTypePtr(vt.Field(i).Type)
// collect rules from sub-struct and from arrays/slices elements
if ft != timeType && reflectx.RemoveValuePtr(vv).IsValid() {
// feat: only descend into NAMED sub-struct fields when the parent field
// carries a `validate` tag (value may be empty); named fields with no tag
// are skipped. Anonymous embedded fields (promoted composition, is-a) are
// part of the parent and always cascade regardless of tag.
if !hasVRuleTag && !fv.Anonymous && gOpt.CheckSubOnParentMarked {
continue
}
fValue := reflectx.RemoveValuePtr(vv).Field(i)
switch ft.Kind() {
case reflect.Struct:
recursiveFunc(fValue, ft, name, fv.Anonymous)
case reflect.Array, reflect.Slice:
fValue = reflectx.RemoveValuePtr(fValue)
// Check if the reflect.Value is valid and not a nil pointer
if !fValue.IsValid() || (ft.Kind() == reflect.Slice && fValue.IsNil()) {
continue
}
// perf: skip parse on elements is simple kind
if reflects.IsSimpleKind(ft.Elem().Kind()) {
continue
}
for j := 0; j < fValue.Len(); j++ {
elemValue := reflectx.RemoveValuePtr(fValue.Index(j))
elemType := removeTypePtr(elemValue.Type())
arrayName := fmt.Sprintf("%s.%d", name, j)
if outName != "" {
fOutMap[arrayName] = fmt.Sprintf("%s.%d", outName, j)
}
if elemType.Kind() == reflect.Struct {
recursiveFunc(elemValue, elemType, arrayName, fv.Anonymous)
}
}
case reflect.Map:
fValue = reflectx.RemoveValuePtr(fValue)
// Check if the reflect.Value is valid and not a nil pointer
if !fValue.IsValid() || fValue.IsNil() {
continue
}
for _, key := range fValue.MapKeys() {
key = reflectx.RemoveValuePtr(key)
elemValue := reflectx.RemoveValuePtr(fValue.MapIndex(key))
elemType := removeTypePtr(elemValue.Type())
format := "%s."
kind := key.Kind()
val := key.Interface()
switch {
case kind == reflect.String:
format += "%s"
val = strings.ReplaceAll(key.String(), "\"", "")
case kind >= reflect.Int && kind <= reflect.Uint64:
format += "%d"
case kind >= reflect.Float32 && kind <= reflect.Complex128:
format += "%f"
default:
format += "%#v"
}
arrayName := fmt.Sprintf(format, name, val)
if outName != "" {
fOutMap[arrayName] = fmt.Sprintf(format, outName, val)
}
if elemType.Kind() == reflect.Struct {
recursiveFunc(elemValue, elemType, arrayName, fv.Anonymous)
}
}
case reflect.Ptr:
// If the field is a pointer type and is nil, and has validation rules, initialize the pointer
if fValue.IsNil() && vRule != "" {
// Create an instance of the type pointed to by the pointer
newValue := reflect.New(ft.Elem())
// Set the field value
reflectx.RemoveValuePtr(vv).Field(i).Set(newValue)
// Update fValue to the newly created value
fValue = newValue
}
// Continue processing the type pointed to by the pointer
if fValue.IsValid() && !fValue.IsNil() && removeTypePtr(ft).Kind() == reflect.Struct {
recursiveFunc(reflectx.RemoveValuePtr(fValue), removeTypePtr(ft), name, fv.Anonymous)
}
default:
// do nothing
}
}
}
}
recursiveFunc(reflectx.RemoveValuePtr(vv), vt, "", false)
if len(fOutMap) > 0 {
v.Trans().AddFieldMap(fOutMap)
}
}
// eg: `message:"required:name is required|minLen:name min len is %d"`
func (d *StructData) loadMessagesFromTag(trans *Translator, field, vRule, vMsg string) {
var msgKey, vName string
var vNames []string
// only one message, use for first validator.
// eg: `message:"name is required"`
if !strings.ContainsRune(vMsg, '|') {
// eg: `message:"required:name is required"`
if strings.ContainsRune(vMsg, ':') {
nodes := strings.SplitN(vMsg, ":", 2)
vName = strings.TrimSpace(nodes[0])
vNames = []string{vName}
// first is validator name
vMsg = strings.TrimSpace(nodes[1])
}
if vName == "" {
// eg `validate:"required|date"`
vNames = []string{vRule}
if strings.ContainsRune(vRule, '|') {
vNames = strings.Split(vRule, "|")
}
for i, node := range vNames {
// has params for validator: "minLen:5"
if strings.ContainsRune(node, ':') {
tmp := strings.SplitN(node, ":", 2)
vNames[i] = tmp[0]
}
}
}
// if rName, has := validatorAliases[validator]; has {
// msgKey = field + "." + rName
// } else {
for _, name := range vNames {
msgKey = field + "." + name
trans.AddMessage(msgKey, vMsg)
}
return
}
// multi message for validators
// eg: `message:"required:name is required | minLen:name min len is %d"`
for _, validatorWithMsg := range strings.Split(vMsg, "|") {
// validatorWithMsg eg: "required:name is required"
nodes := strings.SplitN(validatorWithMsg, ":", 2)
validator := nodes[0]
msgKey = field + "." + validator
trans.AddMessage(msgKey, strings.TrimSpace(nodes[1]))
}
}