From c85a19e59184d3de65a2e0140e35eb88c802bd31 Mon Sep 17 00:00:00 2001 From: Paramtamtam <7326800+tarampampam@users.noreply.github.com> Date: Sat, 28 Oct 2023 02:25:13 +0400 Subject: [PATCH 1/3] add "omitnil" tag --- baked_in.go | 1 + cache.go | 5 +++++ doc.go | 7 +++++++ validator.go | 24 ++++++++++++++++++++++++ validator_instance.go | 1 + validator_test.go | 37 ++++++++++++++++++++++++++++++++++--- 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/baked_in.go b/baked_in.go index 0b6233070..e3b59d1cc 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 230ab61b9..981bf959e 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13154,14 +13154,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}, @@ -13171,7 +13171,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}, @@ -13564,3 +13564,34 @@ func TestTimeRequired(t *testing.T) { NotEqual(t, err, nil) AssertError(t, err.(ValidationErrors), "TestTime.Time", "TestTime.Time", "Time", "Time", "required") } + +func TestOmitNil(t *testing.T) { + type S struct { + Int int `validate:"required,omitnil,min=10"` + IntPtr *int `validate:"required,omitnil,min=10"` + } + + var ( + validate = New(WithRequiredStructEnabled()) + invalid = 9 + valid = 10 + ) + + t.Run("fully valid", func(t *testing.T) { + Equal(t, validate.Struct(S{Int: valid, IntPtr: &valid}), nil) + }) + + t.Run("invalid value in the ptr-field", func(t *testing.T) { + err := validate.Struct(S{Int: valid, IntPtr: &invalid}) + AssertError(t, err.(ValidationErrors), "S.IntPtr", "S.IntPtr", "IntPtr", "IntPtr", "min") + }) + + t.Run("invalid value in the non-ptr field", func(t *testing.T) { + err := validate.Struct(&S{Int: invalid, IntPtr: &valid}) + AssertError(t, err.(ValidationErrors), "S.Int", "S.Int", "Int", "Int", "min") + }) + + t.Run("empty ptr field is ignored", func(t *testing.T) { + Equal(t, validate.Struct(S{Int: valid}), nil) + }) +} From 4f483cbfbb20594bdcec478f29d62d904c68f8dc Mon Sep 17 00:00:00 2001 From: Paramtamtam <7326800+tarampampam@users.noreply.github.com> Date: Sat, 28 Oct 2023 03:04:54 +0400 Subject: [PATCH 2/3] tests updated --- validator_test.go | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/validator_test.go b/validator_test.go index 981bf959e..bd5c2167c 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13567,31 +13567,37 @@ func TestTimeRequired(t *testing.T) { func TestOmitNil(t *testing.T) { type S struct { - Int int `validate:"required,omitnil,min=10"` - IntPtr *int `validate:"required,omitnil,min=10"` + Str string `validate:"omitnil,required,min=10"` + StrPtr *string `validate:"omitnil,required,min=10"` } var ( validate = New(WithRequiredStructEnabled()) - invalid = 9 - valid = 10 + invalid = "too short" + valid = "this is the long string to pass the validation rule" + // zeroStr string ) t.Run("fully valid", func(t *testing.T) { - Equal(t, validate.Struct(S{Int: valid, IntPtr: &valid}), nil) + Equal(t, validate.Struct(S{Str: valid, StrPtr: &valid}), nil) }) - t.Run("invalid value in the ptr-field", func(t *testing.T) { - err := validate.Struct(S{Int: valid, IntPtr: &invalid}) - AssertError(t, err.(ValidationErrors), "S.IntPtr", "S.IntPtr", "IntPtr", "IntPtr", "min") + t.Run("invalid value in the ptr-field only", func(t *testing.T) { + err := validate.Struct(S{Str: valid, StrPtr: &invalid}) + AssertError(t, err, "S.StrPtr", "S.StrPtr", "StrPtr", "StrPtr", "min") }) - t.Run("invalid value in the non-ptr field", func(t *testing.T) { - err := validate.Struct(&S{Int: invalid, IntPtr: &valid}) - AssertError(t, err.(ValidationErrors), "S.Int", "S.Int", "Int", "Int", "min") + t.Run("invalid value in the non-ptr field only", func(t *testing.T) { + err := validate.Struct(&S{Str: invalid, StrPtr: &valid}) + AssertError(t, err, "S.Str", "S.Str", "Str", "Str", "min") }) t.Run("empty ptr field is ignored", func(t *testing.T) { - Equal(t, validate.Struct(S{Int: valid}), nil) + Equal(t, validate.Struct(S{Str: valid}), nil) + }) + + t.Run("fully defaults (using omitempty no error will occur)", func(t *testing.T) { + err := validate.Struct(S{}) + AssertError(t, err, "S.Str", "S.Str", "Str", "Str", "required") }) } From 8a856d61150dc24c92d195a8b0f7a1008b122877 Mon Sep 17 00:00:00 2001 From: Paramtamtam <7326800+tarampampam@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:12:32 +0400 Subject: [PATCH 3/3] tests updated --- validator_test.go | 51 +++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/validator_test.go b/validator_test.go index bd5c2167c..51410e68a 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13565,39 +13565,46 @@ func TestTimeRequired(t *testing.T) { AssertError(t, err.(ValidationErrors), "TestTime.Time", "TestTime.Time", "Time", "Time", "required") } -func TestOmitNil(t *testing.T) { - type S struct { - Str string `validate:"omitnil,required,min=10"` - StrPtr *string `validate:"omitnil,required,min=10"` - } +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()) - invalid = "too short" valid = "this is the long string to pass the validation rule" - // zeroStr string ) - t.Run("fully valid", func(t *testing.T) { - Equal(t, validate.Struct(S{Str: valid, StrPtr: &valid}), nil) - }) + 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}}) - t.Run("invalid value in the ptr-field only", func(t *testing.T) { - err := validate.Struct(S{Str: valid, StrPtr: &invalid}) - AssertError(t, err, "S.StrPtr", "S.StrPtr", "StrPtr", "StrPtr", "min") + Equal(t, err1, nil) + Equal(t, err2, nil) }) - t.Run("invalid value in the non-ptr field only", func(t *testing.T) { - err := validate.Struct(&S{Str: invalid, StrPtr: &valid}) - AssertError(t, err, "S.Str", "S.Str", "Str", "Str", "min") - }) + t.Run("compare fully empty omitempty and omitnil", func(t *testing.T) { + err1 := validate.Struct(OmitEmpty{}) + err2 := validate.Struct(OmitNil{}) - t.Run("empty ptr field is ignored", func(t *testing.T) { - Equal(t, validate.Struct(S{Str: valid}), nil) + Equal(t, err1, nil) + AssertError(t, err2, "OmitNil.Str", "OmitNil.Str", "Str", "Str", "required") }) - t.Run("fully defaults (using omitempty no error will occur)", func(t *testing.T) { - err := validate.Struct(S{}) - AssertError(t, err, "S.Str", "S.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") }) }