diff --git a/baked_in.go b/baked_in.go index 19fa415ed..ac1c350bd 100644 --- a/baked_in.go +++ b/baked_in.go @@ -51,6 +51,7 @@ var ( endKeysTag: {}, structOnlyTag: {}, omitempty: {}, + omitnil: {}, skipValidationTag: {}, utf8HexComma: {}, utf8Pipe: {}, diff --git a/cache.go b/cache.go index bbfd2a4af..0f4fa6b5c 100644 --- a/cache.go +++ b/cache.go @@ -20,6 +20,7 @@ const ( typeOr typeKeys typeEndKeys + typeOmitNil ) const ( @@ -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 diff --git a/doc.go b/doc.go index c4dbb595f..b47409188 100644 --- a/doc.go +++ b/doc.go @@ -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 diff --git a/validator.go b/validator.go index 342c4ec24..a072d39ce 100644 --- a/validator.go +++ b/validator.go @@ -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...)) @@ -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 diff --git a/validator_instance.go b/validator_instance.go index a4dbdd098..d5a7be1de 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -22,6 +22,7 @@ const ( structOnlyTag = "structonly" noStructLevelTag = "nostructlevel" omitempty = "omitempty" + omitnil = "omitnil" isdefault = "isdefault" requiredWithoutAllTag = "required_without_all" requiredWithoutTag = "required_without" diff --git a/validator_test.go b/validator_test.go index 3ea90e29c..e2466118f 100644 --- a/validator_test.go +++ b/validator_test.go @@ -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}, @@ -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}, @@ -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") + }) +}