Skip to content

Commit

Permalink
Add omitnil modifier (#1187)
Browse files Browse the repository at this point in the history
## Fixes Or Enhances

Related issue: #1186

**Make sure that you've checked the boxes below before you submit PR:**
- [x] Tests exist or have been written that cover this particular
change.

@go-playground/validator-maintainers
  • Loading branch information
tarampampam committed Nov 4, 2023
1 parent b0c7337 commit aa96909
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 3 deletions.
1 change: 1 addition & 0 deletions baked_in.go
Expand Up @@ -51,6 +51,7 @@ var (
endKeysTag: {},
structOnlyTag: {},
omitempty: {},
omitnil: {},
skipValidationTag: {},
utf8HexComma: {},
utf8Pipe: {},
Expand Down
5 changes: 5 additions & 0 deletions cache.go
Expand Up @@ -20,6 +20,7 @@ const (
typeOr
typeKeys
typeEndKeys
typeOmitNil
)

const (
Expand Down Expand Up @@ -252,6 +253,10 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
current.typeof = typeOmitEmpty
continue

case omitnil:
current.typeof = typeOmitNil
continue

case structOnlyTag:
current.typeof = typeStructOnly
continue
Expand Down
7 changes: 7 additions & 0 deletions doc.go
Expand Up @@ -194,6 +194,13 @@ such as min or max won't run, but if a value is set validation will run.
Usage: omitempty
# Omit Nil
Allows to skip the validation if the value is nil (same as omitempty, but
only for the nil-values).
Usage: omitnil
# Dive
This tells the validator to dive into a slice, array or map and validate that
Expand Down
24 changes: 24 additions & 0 deletions validator.go
Expand Up @@ -112,6 +112,10 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
return
}

if ct.typeof == typeOmitNil && (kind != reflect.Invalid && current.IsNil()) {
return
}

if ct.hasTag {
if kind == reflect.Invalid {
v.str1 = string(append(ns, cf.altName...))
Expand Down Expand Up @@ -233,6 +237,26 @@ OUTER:
ct = ct.next
continue

case typeOmitNil:
v.slflParent = parent
v.flField = current
v.cf = cf
v.ct = ct

switch field := v.Field(); field.Kind() {
case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
if field.IsNil() {
return
}
default:
if v.fldIsPointer && field.Interface() == nil {
return
}
}

ct = ct.next
continue

case typeEndKeys:
return

Expand Down
1 change: 1 addition & 0 deletions validator_instance.go
Expand Up @@ -22,6 +22,7 @@ const (
structOnlyTag = "structonly"
noStructLevelTag = "nostructlevel"
omitempty = "omitempty"
omitnil = "omitnil"
isdefault = "isdefault"
requiredWithoutAllTag = "required_without_all"
requiredWithoutTag = "required_without"
Expand Down
50 changes: 47 additions & 3 deletions validator_test.go
Expand Up @@ -13196,14 +13196,14 @@ func TestSpiceDBValueFormatValidation(t *testing.T) {
tag string
expected bool
}{
//Must be an asterisk OR a string containing alphanumeric characters and a restricted set a special symbols: _ | / - = +
// Must be an asterisk OR a string containing alphanumeric characters and a restricted set a special symbols: _ | / - = +
{"*", "spicedb=id", true},
{`azAZ09_|/-=+`, "spicedb=id", true},
{`a*`, "spicedb=id", false},
{`/`, "spicedb=id", true},
{"*", "spicedb", true},

//Must begin and end with a lowercase letter, may also contain numbers and underscores between, min length 3, max length 64
// Must begin and end with a lowercase letter, may also contain numbers and underscores between, min length 3, max length 64
{"a", "spicedb=permission", false},
{"1", "spicedb=permission", false},
{"a1", "spicedb=permission", false},
Expand All @@ -13213,7 +13213,7 @@ func TestSpiceDBValueFormatValidation(t *testing.T) {
{"abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", true},
{"abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", false},

//Object types follow the same rules as permissions for the type name plus an optional prefix up to 63 characters with a /
// Object types follow the same rules as permissions for the type name plus an optional prefix up to 63 characters with a /
{"a", "spicedb=type", false},
{"1", "spicedb=type", false},
{"a1", "spicedb=type", false},
Expand Down Expand Up @@ -13606,3 +13606,47 @@ func TestTimeRequired(t *testing.T) {
NotEqual(t, err, nil)
AssertError(t, err.(ValidationErrors), "TestTime.Time", "TestTime.Time", "Time", "Time", "required")
}

func TestOmitNilAndRequired(t *testing.T) {
type (
OmitEmpty struct {
Str string `validate:"omitempty,required,min=10"`
StrPtr *string `validate:"omitempty,required,min=10"`
Inner *OmitEmpty
}
OmitNil struct {
Str string `validate:"omitnil,required,min=10"`
StrPtr *string `validate:"omitnil,required,min=10"`
Inner *OmitNil
}
)

var (
validate = New(WithRequiredStructEnabled())
valid = "this is the long string to pass the validation rule"
)

t.Run("compare using valid data", func(t *testing.T) {
err1 := validate.Struct(OmitEmpty{Str: valid, StrPtr: &valid, Inner: &OmitEmpty{Str: valid, StrPtr: &valid}})
err2 := validate.Struct(OmitNil{Str: valid, StrPtr: &valid, Inner: &OmitNil{Str: valid, StrPtr: &valid}})

Equal(t, err1, nil)
Equal(t, err2, nil)
})

t.Run("compare fully empty omitempty and omitnil", func(t *testing.T) {
err1 := validate.Struct(OmitEmpty{})
err2 := validate.Struct(OmitNil{})

Equal(t, err1, nil)
AssertError(t, err2, "OmitNil.Str", "OmitNil.Str", "Str", "Str", "required")
})

t.Run("validate in deep", func(t *testing.T) {
err1 := validate.Struct(OmitEmpty{Str: valid, Inner: &OmitEmpty{}})
err2 := validate.Struct(OmitNil{Str: valid, Inner: &OmitNil{}})

Equal(t, err1, nil)
AssertError(t, err2, "OmitNil.Inner.Str", "OmitNil.Inner.Str", "Str", "Str", "required")
})
}

0 comments on commit aa96909

Please sign in to comment.