Skip to content

Commit 9b7c4de

Browse files
author
Dean Karn
authored
Struct experiment (#1150)
## PR This PR does the following: - Reverts #1122 - Re-implements struct level validations in a different way which also support `or`s etc.. which previous implementation did not. - Adds special case to ignore `required` validation on non-pointer structs to preserve pre-struct level tag validation support. - Added new `WithRequiredStructEnabled` option to opt-in to this new behaviour, that will become the default in the next major version.
1 parent 3094d59 commit 9b7c4de

File tree

12 files changed

+212
-185
lines changed

12 files changed

+212
-185
lines changed

.github/workflows/workflow.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
test:
99
strategy:
1010
matrix:
11-
go-version: [1.20.x]
11+
go-version: [1.17.x,1.18.x,1.21.x]
1212
os: [ubuntu-latest, macos-latest, windows-latest]
1313
runs-on: ${{ matrix.os }}
1414
steps:
@@ -32,7 +32,7 @@ jobs:
3232
run: go test -race -covermode=atomic -coverprofile="profile.cov" ./...
3333

3434
- name: Send Coverage
35-
if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.20.x'
35+
if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.21.x'
3636
uses: shogo82148/actions-goveralls@v1
3737
with:
3838
path-to-profile: profile.cov
@@ -43,7 +43,7 @@ jobs:
4343
steps:
4444
- uses: actions/setup-go@v3
4545
with:
46-
go-version: 1.19.x
46+
go-version: 1.21.x
4747
- uses: actions/checkout@v3
4848
- name: golangci-lint
4949
uses: golangci/golangci-lint-action@v3

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ test:
1313
$(GOCMD) test -cover -race ./...
1414

1515
bench:
16-
$(GOCMD) test -bench=. -benchmem ./...
16+
$(GOCMD) test -run=NONE -bench=. -benchmem ./...
1717

1818
.PHONY: test lint linters-install

README.md

Lines changed: 72 additions & 65 deletions
Large diffs are not rendered by default.

_examples/simple/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ var validate *validator.Validate
3030

3131
func main() {
3232

33-
validate = validator.New()
33+
validate = validator.New(validator.WithRequiredStructEnabled())
3434

3535
validateStruct()
3636
validateVariable()

baked_in.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"golang.org/x/text/language"
2424

2525
"github.com/gabriel-vasile/mimetype"
26-
"github.com/leodido/go-urn"
26+
urn "github.com/leodido/go-urn"
2727
)
2828

2929
// Func accepts a FieldLevel interface for all validation needs. The return

cache.go

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ const (
2020
typeOr
2121
typeKeys
2222
typeEndKeys
23-
typeNestedStructLevel
2423
)
2524

2625
const (
@@ -153,7 +152,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr
153152
// and so only struct level caching can be used instead of combined with Field tag caching
154153

155154
if len(tag) > 0 {
156-
ctag, _ = v.parseFieldTagsRecursive(tag, fld, "", false)
155+
ctag, _ = v.parseFieldTagsRecursive(tag, fld.Name, "", false)
157156
} else {
158157
// even if field doesn't have validations need cTag for traversing to potential inner/nested
159158
// elements of the field.
@@ -172,7 +171,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr
172171
return cs
173172
}
174173

175-
func (v *Validate) parseFieldTagsRecursive(tag string, field reflect.StructField, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
174+
func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
176175
var t string
177176
noAlias := len(alias) == 0
178177
tags := strings.Split(tag, tagSeparator)
@@ -186,9 +185,9 @@ func (v *Validate) parseFieldTagsRecursive(tag string, field reflect.StructField
186185
// check map for alias and process new tags, otherwise process as usual
187186
if tagsVal, found := v.aliases[t]; found {
188187
if i == 0 {
189-
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, field, t, true)
188+
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
190189
} else {
191-
next, curr := v.parseFieldTagsRecursive(tagsVal, field, t, true)
190+
next, curr := v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
192191
current.next, current = next, curr
193192

194193
}
@@ -236,7 +235,7 @@ func (v *Validate) parseFieldTagsRecursive(tag string, field reflect.StructField
236235
}
237236
}
238237

239-
current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), field, "", false)
238+
current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), fieldName, "", false)
240239
continue
241240

242241
case endKeysTag:
@@ -285,18 +284,14 @@ func (v *Validate) parseFieldTagsRecursive(tag string, field reflect.StructField
285284

286285
current.tag = vals[0]
287286
if len(current.tag) == 0 {
288-
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, field.Name)))
287+
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, fieldName)))
289288
}
290289

291290
if wrapper, ok := v.validations[current.tag]; ok {
292291
current.fn = wrapper.fn
293292
current.runValidationWhenNil = wrapper.runValidatinOnNil
294293
} else {
295-
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, field.Name)))
296-
}
297-
298-
if current.typeof == typeDefault && isNestedStructOrStructPtr(field) {
299-
current.typeof = typeNestedStructLevel
294+
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, fieldName)))
300295
}
301296

302297
if len(orVals) > 1 {
@@ -324,7 +319,7 @@ func (v *Validate) fetchCacheTag(tag string) *cTag {
324319
// isn't parsed again.
325320
ctag, found = v.tagCache.Get(tag)
326321
if !found {
327-
ctag, _ = v.parseFieldTagsRecursive(tag, reflect.StructField{}, "", false)
322+
ctag, _ = v.parseFieldTagsRecursive(tag, "", "", false)
328323
v.tagCache.Set(tag, ctag)
329324
}
330325
}

doc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ Example #2
247247
This validates that the value is not the data types default zero value.
248248
For numbers ensures value is not zero. For strings ensures value is
249249
not "". For slices, maps, pointers, interfaces, channels and functions
250-
ensures the value is not nil. For structs ensures value is not the zero value.
250+
ensures the value is not nil. For structs ensures value is not the zero value when using WithRequiredStructEnabled.
251251
252252
Usage: required
253253

options.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package validator
2+
3+
// Option represents a configurations option to be applied to validator during initialization.
4+
type Option func(*Validate)
5+
6+
// WithRequiredStructEnabled enables required tag on non-pointer structs to be applied instead of ignored.
7+
//
8+
// This was made opt-in behaviour in order to maintain backward compatibility with the behaviour previous
9+
// to being able to apply struct level validations on struct fields directly.
10+
//
11+
// It is recommended you enabled this as it will be the default behaviour in v11+
12+
func WithRequiredStructEnabled() Option {
13+
return func(v *Validate) {
14+
v.requiredStructEnabled = true
15+
}
16+
}

util.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,3 @@ func panicIf(err error) {
292292
panic(err.Error())
293293
}
294294
}
295-
296-
func isNestedStructOrStructPtr(v reflect.StructField) bool {
297-
if v.Type == nil {
298-
return false
299-
}
300-
kind := v.Type.Kind()
301-
return kind == reflect.Struct || kind == reflect.Ptr && v.Type.Elem().Kind() == reflect.Struct
302-
}

validator.go

Lines changed: 42 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
9999

100100
current, kind, v.fldIsPointer = v.extractTypeInternal(current, false)
101101

102+
var isNestedStruct bool
103+
102104
switch kind {
103105
case reflect.Ptr, reflect.Interface, reflect.Invalid:
104106

@@ -161,85 +163,56 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
161163
}
162164

163165
case reflect.Struct:
164-
165-
typ = current.Type()
166-
167-
if !typ.ConvertibleTo(timeType) {
168-
169-
if ct != nil {
170-
171-
if ct.typeof == typeStructOnly {
172-
goto CONTINUE
173-
} else if ct.typeof == typeIsDefault || ct.typeof == typeNestedStructLevel {
174-
// set Field Level fields
175-
v.slflParent = parent
176-
v.flField = current
177-
v.cf = cf
178-
v.ct = ct
179-
180-
if !ct.fn(ctx, v) {
181-
v.str1 = string(append(ns, cf.altName...))
182-
183-
if v.v.hasTagNameFunc {
184-
v.str2 = string(append(structNs, cf.name...))
185-
} else {
186-
v.str2 = v.str1
187-
}
188-
189-
v.errs = append(v.errs,
190-
&fieldError{
191-
v: v.v,
192-
tag: ct.aliasTag,
193-
actualTag: ct.tag,
194-
ns: v.str1,
195-
structNs: v.str2,
196-
fieldLen: uint8(len(cf.altName)),
197-
structfieldLen: uint8(len(cf.name)),
198-
value: current.Interface(),
199-
param: ct.param,
200-
kind: kind,
201-
typ: typ,
202-
},
203-
)
204-
return
205-
}
206-
}
207-
208-
ct = ct.next
209-
}
210-
211-
if ct != nil && ct.typeof == typeNoStructLevel {
212-
return
213-
}
214-
215-
CONTINUE:
216-
// if len == 0 then validating using 'Var' or 'VarWithValue'
217-
// Var - doesn't make much sense to do it that way, should call 'Struct', but no harm...
218-
// VarWithField - this allows for validating against each field within the struct against a specific value
219-
// pretty handy in certain situations
220-
if len(cf.name) > 0 {
221-
ns = append(append(ns, cf.altName...), '.')
222-
structNs = append(append(structNs, cf.name...), '.')
223-
}
224-
225-
v.validateStruct(ctx, parent, current, typ, ns, structNs, ct)
226-
return
166+
isNestedStruct = !current.Type().ConvertibleTo(timeType)
167+
// For backward compatibility before struct level validation tags were supported
168+
// as there were a number of projects relying on `required` not failing on non-pointer
169+
// structs. Since it's basically nonsensical to use `required` with a non-pointer struct
170+
// are explicitly skipping the required validation for it. This WILL be removed in the
171+
// next major version.
172+
if !v.v.requiredStructEnabled && ct != nil && ct.tag == requiredTag {
173+
ct = ct.next
227174
}
228175
}
229176

230-
if ct == nil || !ct.hasTag {
231-
return
232-
}
233-
234177
typ = current.Type()
235178

236179
OUTER:
237180
for {
238-
if ct == nil {
181+
if ct == nil || !ct.hasTag || (isNestedStruct && len(cf.name) == 0) {
182+
// isNestedStruct check here
183+
if isNestedStruct {
184+
// if len == 0 then validating using 'Var' or 'VarWithValue'
185+
// Var - doesn't make much sense to do it that way, should call 'Struct', but no harm...
186+
// VarWithField - this allows for validating against each field within the struct against a specific value
187+
// pretty handy in certain situations
188+
if len(cf.name) > 0 {
189+
ns = append(append(ns, cf.altName...), '.')
190+
structNs = append(append(structNs, cf.name...), '.')
191+
}
192+
193+
v.validateStruct(ctx, parent, current, typ, ns, structNs, ct)
194+
}
239195
return
240196
}
241197

242198
switch ct.typeof {
199+
case typeNoStructLevel:
200+
return
201+
202+
case typeStructOnly:
203+
if isNestedStruct {
204+
// if len == 0 then validating using 'Var' or 'VarWithValue'
205+
// Var - doesn't make much sense to do it that way, should call 'Struct', but no harm...
206+
// VarWithField - this allows for validating against each field within the struct against a specific value
207+
// pretty handy in certain situations
208+
if len(cf.name) > 0 {
209+
ns = append(append(ns, cf.altName...), '.')
210+
structNs = append(append(structNs, cf.name...), '.')
211+
}
212+
213+
v.validateStruct(ctx, parent, current, typ, ns, structNs, ct)
214+
}
215+
return
243216

244217
case typeOmitEmpty:
245218

@@ -366,7 +339,7 @@ OUTER:
366339
ct = ct.next
367340

368341
if ct == nil {
369-
return
342+
continue OUTER
370343
}
371344

372345
if ct.typeof != typeOr {

0 commit comments

Comments
 (0)