Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: nested struct validation #1122

Merged
merged 5 commits into from
Aug 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions baked_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -1710,7 +1710,7 @@ func hasValue(fl FieldLevel) bool {
if fl.(*validate).fldIsPointer && field.Interface() != nil {
return true
}
return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface()
return field.IsValid() && !field.IsZero()
}
}

Expand All @@ -1734,7 +1734,7 @@ func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue boo
if nullable && field.Interface() != nil {
return false
}
return field.IsValid() && field.Interface() == reflect.Zero(field.Type()).Interface()
return field.IsValid() && field.IsZero()
}
}

Expand Down
21 changes: 13 additions & 8 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
typeOr
typeKeys
typeEndKeys
typeNestedStructLevel
)

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

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

func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
func (v *Validate) parseFieldTagsRecursive(tag string, field reflect.StructField, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
var t string
noAlias := len(alias) == 0
tags := strings.Split(tag, tagSeparator)
Expand All @@ -185,9 +186,9 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
// check map for alias and process new tags, otherwise process as usual
if tagsVal, found := v.aliases[t]; found {
if i == 0 {
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, field, t, true)
} else {
next, curr := v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
next, curr := v.parseFieldTagsRecursive(tagsVal, field, t, true)
current.next, current = next, curr

}
Expand Down Expand Up @@ -235,7 +236,7 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
}
}

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

case endKeysTag:
Expand Down Expand Up @@ -284,14 +285,18 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s

current.tag = vals[0]
if len(current.tag) == 0 {
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, fieldName)))
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, field.Name)))
}

if wrapper, ok := v.validations[current.tag]; ok {
current.fn = wrapper.fn
current.runValidationWhenNil = wrapper.runValidatinOnNil
} else {
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, fieldName)))
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, field.Name)))
}

if current.typeof == typeDefault && isNestedStructOrStructPtr(field) {
current.typeof = typeNestedStructLevel
}

if len(orVals) > 1 {
Expand Down Expand Up @@ -319,7 +324,7 @@ func (v *Validate) fetchCacheTag(tag string) *cTag {
// isn't parsed again.
ctag, found = v.tagCache.Get(tag)
if !found {
ctag, _ = v.parseFieldTagsRecursive(tag, "", "", false)
ctag, _ = v.parseFieldTagsRecursive(tag, reflect.StructField{}, "", false)
v.tagCache.Set(tag, ctag)
}
}
Expand Down
18 changes: 9 additions & 9 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ Example #2
This validates that the value is not the data types default zero value.
For numbers ensures value is not zero. For strings ensures value is
not "". For slices, maps, pointers, interfaces, channels and functions
ensures the value is not nil.
ensures the value is not nil. For structs ensures value is not the zero value.

Usage: required

Expand All @@ -256,7 +256,7 @@ ensures the value is not nil.
The field under validation must be present and not empty only if all
the other specified fields are equal to the value following the specified
field. For strings ensures value is not "". For slices, maps, pointers,
interfaces, channels and functions ensures the value is not nil.
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.

Usage: required_if

Expand All @@ -273,7 +273,7 @@ Examples:
The field under validation must be present and not empty unless all
the other specified fields are equal to the value following the specified
field. For strings ensures value is not "". For slices, maps, pointers,
interfaces, channels and functions ensures the value is not nil.
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.

Usage: required_unless

Expand All @@ -290,7 +290,7 @@ Examples:
The field under validation must be present and not empty only if any
of the other specified fields are present. For strings ensures value is
not "". For slices, maps, pointers, interfaces, channels and functions
ensures the value is not nil.
ensures the value is not nil. For structs ensures value is not the zero value.

Usage: required_with

Expand All @@ -307,7 +307,7 @@ Examples:
The field under validation must be present and not empty only if all
of the other specified fields are present. For strings ensures value is
not "". For slices, maps, pointers, interfaces, channels and functions
ensures the value is not nil.
ensures the value is not nil. For structs ensures value is not the zero value.

Usage: required_with_all

Expand All @@ -321,7 +321,7 @@ Example:
The field under validation must be present and not empty only when any
of the other specified fields are not present. For strings ensures value is
not "". For slices, maps, pointers, interfaces, channels and functions
ensures the value is not nil.
ensures the value is not nil. For structs ensures value is not the zero value.

Usage: required_without

Expand All @@ -338,7 +338,7 @@ Examples:
The field under validation must be present and not empty only when all
of the other specified fields are not present. For strings ensures value is
not "". For slices, maps, pointers, interfaces, channels and functions
ensures the value is not nil.
ensures the value is not nil. For structs ensures value is not the zero value.

Usage: required_without_all

Expand All @@ -352,7 +352,7 @@ Example:
The field under validation must not be present or not empty only if all
the other specified fields are equal to the value following the specified
field. For strings ensures value is not "". For slices, maps, pointers,
interfaces, channels and functions ensures the value is not nil.
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.

Usage: excluded_if

Expand All @@ -369,7 +369,7 @@ Examples:
The field under validation must not be present or empty unless all
the other specified fields are equal to the value following the specified
field. For strings ensures value is not "". For slices, maps, pointers,
interfaces, channels and functions ensures the value is not nil.
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.

Usage: excluded_unless

Expand Down
8 changes: 8 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,11 @@ func panicIf(err error) {
panic(err.Error())
}
}

func isNestedStructOrStructPtr(v reflect.StructField) bool {
if v.Type == nil {
return false
}
kind := v.Type.Kind()
return kind == reflect.Struct || kind == reflect.Ptr && v.Type.Elem().Kind() == reflect.Struct
}
2 changes: 1 addition & 1 deletion validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr

if ct.typeof == typeStructOnly {
goto CONTINUE
} else if ct.typeof == typeIsDefault {
} else if ct.typeof == typeIsDefault || ct.typeof == typeNestedStructLevel {
// set Field Level fields
v.slflParent = parent
v.flField = current
Expand Down
Loading