From 58330a7c94c254d90d49a2f008778cd4b708376b Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Thu, 25 Apr 2024 02:20:49 -0300 Subject: [PATCH 1/3] feat: add required_if_contains and excluded_if_contains --- README.md | 2 + baked_in.go | 64 +++++++++++++++++++++++ doc.go | 38 ++++++++++++++ validator_instance.go | 6 ++- validator_test.go | 116 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a6e1d0b51..afbb63a5f 100644 --- a/README.md +++ b/README.md @@ -244,12 +244,14 @@ validate := validator.New(validator.WithRequiredStructEnabled()) | oneof | One Of | | required | Required | | required_if | Required If | +| required_if_contains | Required If Contains | | required_unless | Required Unless | | required_with | Required With | | required_with_all | Required With All | | required_without | Required Without | | required_without_all | Required Without All | | excluded_if | Excluded If | +| excluded_if_contains | Excluded If Contains | | excluded_unless | Excluded Unless | | excluded_with | Excluded With | | excluded_with_all | Excluded With All | diff --git a/baked_in.go b/baked_in.go index 95f56e008..1c2da529b 100644 --- a/baked_in.go +++ b/baked_in.go @@ -74,6 +74,7 @@ var ( bakedInValidators = map[string]Func{ "required": hasValue, "required_if": requiredIf, + "required_if_contains": requiredIfContains, "required_unless": requiredUnless, "skip_unless": skipUnless, "required_with": requiredWith, @@ -81,6 +82,7 @@ var ( "required_without": requiredWithout, "required_without_all": requiredWithoutAll, "excluded_if": excludedIf, + "excluded_if_contains": excludedIfContains, "excluded_unless": excludedUnless, "excluded_with": excludedWith, "excluded_with_all": excludedWithAll, @@ -1792,12 +1794,23 @@ func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue boo // requireCheckFieldValue is a func for check field value func requireCheckFieldValue( fl FieldLevel, param string, value string, defaultNotFoundValue bool, +) bool { + return requireCheckFieldValues(fl, param, value, defaultNotFoundValue, false) +} + +// requireCheckFieldValue is a func for check field value +func requireCheckFieldValues( + fl FieldLevel, param string, value string, defaultNotFoundValue bool, sliceContains bool, ) bool { field, kind, _, found := fl.GetStructFieldOKAdvanced2(fl.Parent(), param) if !found { return defaultNotFoundValue } + return compareValues(field, kind, value, sliceContains) +} + +func compareValues(field reflect.Value, kind reflect.Kind, value string, sliceContains bool) bool { switch kind { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: @@ -1813,8 +1826,28 @@ func requireCheckFieldValue( return field.Float() == asFloat64(value) case reflect.Slice, reflect.Map, reflect.Array: + // If slice contains is true, should look for the value inside the slice + if sliceContains { + for i := 0; i < field.Len(); i++ { + item := field.Index(i) + if compareValues(item, item.Kind(), value, false) { + return true + } + } + + return false + } + return int64(field.Len()) == asInt(value) + case reflect.Ptr: + if field.IsNil() { + return false + } + + element := field.Elem() + + return compareValues(element, element.Kind(), value, false) case reflect.Bool: return field.Bool() == asBool(value) } @@ -1838,6 +1871,21 @@ func requiredIf(fl FieldLevel) bool { return hasValue(fl) } +// requiredIfContains is the validation function +// The field under validation must be present and not empty only if all the other specified fields are equal to the value following with the specified field. +func requiredIfContains(fl FieldLevel) bool { + params := parseOneOfParam2(fl.Param()) + if len(params)%2 != 0 { + panic(fmt.Sprintf("Bad param number for required_if_contains %s", fl.FieldName())) + } + for i := 0; i < len(params); i += 2 { + if !requireCheckFieldValues(fl, params[i], params[i+1], false, true) { + return true + } + } + return hasValue(fl) +} + // excludedIf is the validation function // The field under validation must not be present or is empty only if all the other specified fields are equal to the value following with the specified field. func excludedIf(fl FieldLevel) bool { @@ -1854,6 +1902,22 @@ func excludedIf(fl FieldLevel) bool { return !hasValue(fl) } +// excludedIfContains is the validation function +// The field under validation must not be present or is empty only if all the other specified fields are equal to the value following with the specified field. +func excludedIfContains(fl FieldLevel) bool { + params := parseOneOfParam2(fl.Param()) + if len(params)%2 != 0 { + panic(fmt.Sprintf("Bad param number for excluded_if_contains %s", fl.FieldName())) + } + + for i := 0; i < len(params); i += 2 { + if !requireCheckFieldValues(fl, params[i], params[i+1], false, true) { + return true + } + } + return !hasValue(fl) +} + // requiredUnless is the validation function // The field under validation must be present and not empty only unless all the other specified fields are equal to the value following with the specified field. func requiredUnless(fl FieldLevel) bool { diff --git a/doc.go b/doc.go index b47409188..afe812669 100644 --- a/doc.go +++ b/doc.go @@ -275,6 +275,25 @@ Examples: // require the field if the Field1 and Field2 is equal to the value respectively: Usage: required_if=Field1 foo Field2 bar +# Required If Contains + +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. For structs ensures value is not the zero value. + +Diferent from required_if, this tag will dive into slices to check if the value is present instead of checking the slice size. + + Usage: required_if_contains + +Examples: + + // require the field if the Field1 contains the parameter given: + Usage: required_if_contains=Field1 foobar + + // require the field if the Field1 and Field2 contains the value respectively: + Usage: required_if_contains=Field1 foo Field2 bar + # Required Unless The field under validation must be present and not empty unless all @@ -371,6 +390,25 @@ Examples: // exclude the field if the Field1 and Field2 is equal to the value respectively: Usage: excluded_if=Field1 foo Field2 bar +# Excluded If Contains + +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. For structs ensures value is not the zero value. + +Diferent from excluded_if, this tag will dive into slices to check if the value is present instead of checking the slice size. + + Usage: excluded_if_contains + +Examples: + + // exclude the field if the Field1 contains the parameter given: + Usage: excluded_if_contains=Field1 foobar + + // exclude the field if the Field1 and Field2 contains the value respectively: + Usage: excluded_if_contains=Field1 foo Field2 bar + # Excluded Unless The field under validation must not be present or empty unless all diff --git a/validator_instance.go b/validator_instance.go index 1a345138e..db686a9b1 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -29,6 +29,7 @@ const ( requiredWithTag = "required_with" requiredWithAllTag = "required_with_all" requiredIfTag = "required_if" + requiredIfContainsTag = "required_if_contains" requiredUnlessTag = "required_unless" skipUnlessTag = "skip_unless" excludedWithoutAllTag = "excluded_without_all" @@ -36,6 +37,7 @@ const ( excludedWithTag = "excluded_with" excludedWithAllTag = "excluded_with_all" excludedIfTag = "excluded_if" + excludedIfContainsTag = "excluded_if_contains" excludedUnlessTag = "excluded_unless" skipValidationTag = "-" diveTag = "dive" @@ -128,8 +130,8 @@ func New(options ...Option) *Validate { switch k { // these require that even if the value is nil that the validation should run, omitempty still overrides this behaviour - case requiredIfTag, requiredUnlessTag, requiredWithTag, requiredWithAllTag, requiredWithoutTag, requiredWithoutAllTag, - excludedIfTag, excludedUnlessTag, excludedWithTag, excludedWithAllTag, excludedWithoutTag, excludedWithoutAllTag, + case requiredIfContainsTag, requiredIfTag, requiredUnlessTag, requiredWithTag, requiredWithAllTag, requiredWithoutTag, requiredWithoutAllTag, + excludedIfContainsTag, excludedIfTag, excludedUnlessTag, excludedWithTag, excludedWithAllTag, excludedWithoutTag, excludedWithoutAllTag, skipUnlessTag: _ = v.registerValidation(k, wrapFunc(val), true, true) default: diff --git a/validator_test.go b/validator_test.go index 3b6d26348..85e4c4380 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13794,3 +13794,119 @@ func TestPrivateFieldsStruct(t *testing.T) { Equal(t, len(errs), tc.errorNum) } } + +func TestRequiredIfContains(t *testing.T) { + type Inner struct { + Field []string + } + + fieldVal := "test" + fieldValInt := 1 + type test struct { + InnerP *Inner + Inner Inner + FieldS []string `validate:"omitempty" json:"field_e"` + FieldI []int `validate:"omitempty" json:"field_i"` + FieldIP []*int `validate:"omitempty" json:"field_ip"` + FieldTS string `validate:"required_if_contains=FieldS test" json:"field_ts"` + FieldTI string `validate:"required_if_contains=FieldI 1" json:"field_ti"` + FieldTIP string `validate:"required_if_contains=FieldIP 1" json:"field_tip"` + FieldTInnerP string `validate:"required_if_contains=InnerP.Field test" json:"field_t_innerp"` + FieldTInner string `validate:"required_if_contains=Inner.Field test" json:"field_t_inner"` + } + + validationOk := test{ + InnerP: &Inner{Field: []string{fieldVal}}, + Inner: Inner{Field: []string{fieldVal}}, + FieldS: []string{fieldVal}, + FieldI: []int{1}, + FieldTS: fieldVal, + FieldTI: fieldVal, + FieldTIP: fieldVal, + FieldTInnerP: fieldVal, + FieldTInner: fieldVal, + } + + validate := New() + + errs := validate.Struct(validationOk) + Equal(t, errs, nil) + + validationNotOk := test{ + InnerP: &Inner{Field: []string{fieldVal}}, + Inner: Inner{Field: []string{fieldVal}}, + FieldS: []string{fieldVal}, + FieldI: []int{1}, + FieldIP: []*int{&fieldValInt}, + } + + errs = validate.Struct(validationNotOk) + NotEqual(t, errs, nil) + + ve := errs.(ValidationErrors) + Equal(t, len(ve), 5) + + AssertError(t, errs, "test.FieldTS", "test.FieldTS", "FieldTS", "FieldTS", "required_if_contains") + AssertError(t, errs, "test.FieldTI", "test.FieldTI", "FieldTI", "FieldTI", "required_if_contains") + AssertError(t, errs, "test.FieldTIP", "test.FieldTIP", "FieldTIP", "FieldTIP", "required_if_contains") + AssertError(t, errs, "test.FieldTInnerP", "test.FieldTInnerP", "FieldTInnerP", "FieldTInnerP", "required_if_contains") + AssertError(t, errs, "test.FieldTInner", "test.FieldTInner", "FieldTInner", "FieldTInner", "required_if_contains") +} + +func TestExcludeIfContains(t *testing.T) { + type Inner struct { + Field []string + } + + fieldVal := "test" + fieldValInt := 1 + type test struct { + InnerP *Inner + Inner Inner + FieldS []string `validate:"omitempty" json:"field_e"` + FieldI []int `validate:"omitempty" json:"field_i"` + FieldIP []*int `validate:"omitempty" json:"field_ip"` + FieldTS string `validate:"excluded_if_contains=FieldS test" json:"field_ts"` + FieldTI string `validate:"excluded_if_contains=FieldI 1" json:"field_ti"` + FieldTIP string `validate:"excluded_if_contains=FieldIP 1" json:"field_tip"` + FieldTInnerP string `validate:"excluded_if_contains=InnerP.Field test" json:"field_t_innerp"` + FieldTInner string `validate:"excluded_if_contains=Inner.Field test" json:"field_t_inner"` + } + + validationOk := test{ + InnerP: &Inner{Field: []string{fieldVal}}, + Inner: Inner{Field: []string{fieldVal}}, + FieldS: []string{fieldVal}, + FieldI: []int{1}, + } + + validate := New() + + errs := validate.Struct(validationOk) + Equal(t, errs, nil) + + validationNotOk := test{ + InnerP: &Inner{Field: []string{fieldVal}}, + Inner: Inner{Field: []string{fieldVal}}, + FieldS: []string{fieldVal}, + FieldI: []int{1}, + FieldIP: []*int{&fieldValInt}, + FieldTS: fieldVal, + FieldTI: fieldVal, + FieldTIP: fieldVal, + FieldTInnerP: fieldVal, + FieldTInner: fieldVal, + } + + errs = validate.Struct(validationNotOk) + NotEqual(t, errs, nil) + + ve := errs.(ValidationErrors) + Equal(t, len(ve), 5) + + AssertError(t, errs, "test.FieldTS", "test.FieldTS", "FieldTS", "FieldTS", "excluded_if_contains") + AssertError(t, errs, "test.FieldTI", "test.FieldTI", "FieldTI", "FieldTI", "excluded_if_contains") + AssertError(t, errs, "test.FieldTIP", "test.FieldTIP", "FieldTIP", "FieldTIP", "excluded_if_contains") + AssertError(t, errs, "test.FieldTInnerP", "test.FieldTInnerP", "FieldTInnerP", "FieldTInnerP", "excluded_if_contains") + AssertError(t, errs, "test.FieldTInner", "test.FieldTInner", "FieldTInner", "FieldTInner", "excluded_if_contains") +} From fd2425ea3c1c381626c479b8f6dcbbd9d4b7596f Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Thu, 25 Apr 2024 02:34:25 -0300 Subject: [PATCH 2/3] feat: add excluded_unless_contains --- baked_in.go | 16 +++++++++ doc.go | 19 +++++++++++ validator_instance.go | 79 ++++++++++++++++++++++--------------------- validator_test.go | 57 +++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 39 deletions(-) diff --git a/baked_in.go b/baked_in.go index 1c2da529b..a22796099 100644 --- a/baked_in.go +++ b/baked_in.go @@ -84,6 +84,7 @@ var ( "excluded_if": excludedIf, "excluded_if_contains": excludedIfContains, "excluded_unless": excludedUnless, + "excluded_unless_contains": excludedUnlessContains, "excluded_with": excludedWith, "excluded_with_all": excludedWithAll, "excluded_without": excludedWithout, @@ -1964,6 +1965,21 @@ func excludedUnless(fl FieldLevel) bool { return true } +// excludedUnless is the validation function +// The field under validation must not be present or is empty unless all the other specified fields are equal to the value following with the specified field. +func excludedUnlessContains(fl FieldLevel) bool { + params := parseOneOfParam2(fl.Param()) + if len(params)%2 != 0 { + panic(fmt.Sprintf("Bad param number for excluded_unless_contains %s", fl.FieldName())) + } + for i := 0; i < len(params); i += 2 { + if !requireCheckFieldValues(fl, params[i], params[i+1], false, true) { + return !hasValue(fl) + } + } + return true +} + // excludedWith is the validation function // The field under validation must not be present or is empty if any of the other specified fields are present. func excludedWith(fl FieldLevel) bool { diff --git a/doc.go b/doc.go index afe812669..27aa88080 100644 --- a/doc.go +++ b/doc.go @@ -426,6 +426,25 @@ Examples: // exclude the field unless the Field1 and Field2 is equal to the value respectively: Usage: excluded_unless=Field1 foo Field2 bar +# Excluded Unless Contains + +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. For structs ensures value is not the zero value. + +Diferent from excluded_unless, this tag will dive into slices to check if the value is present instead of checking the slice size. + + Usage: excluded_unless_contains + +Examples: + + // exclude the field unless the Field1 contains the parameter given: + Usage: excluded_unless_contains=Field1 foobar + + // exclude the field unless the Field1 and Field2 contains the value respectively: + Usage: excluded_unless_contains=Field1 foo Field2 bar + # Is Default This validates that the value is the default value and is almost the diff --git a/validator_instance.go b/validator_instance.go index db686a9b1..b071fa7f3 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -13,43 +13,44 @@ import ( ) const ( - defaultTagName = "validate" - utf8HexComma = "0x2C" - utf8Pipe = "0x7C" - tagSeparator = "," - orSeparator = "|" - tagKeySeparator = "=" - structOnlyTag = "structonly" - noStructLevelTag = "nostructlevel" - omitempty = "omitempty" - omitnil = "omitnil" - isdefault = "isdefault" - requiredWithoutAllTag = "required_without_all" - requiredWithoutTag = "required_without" - requiredWithTag = "required_with" - requiredWithAllTag = "required_with_all" - requiredIfTag = "required_if" - requiredIfContainsTag = "required_if_contains" - requiredUnlessTag = "required_unless" - skipUnlessTag = "skip_unless" - excludedWithoutAllTag = "excluded_without_all" - excludedWithoutTag = "excluded_without" - excludedWithTag = "excluded_with" - excludedWithAllTag = "excluded_with_all" - excludedIfTag = "excluded_if" - excludedIfContainsTag = "excluded_if_contains" - excludedUnlessTag = "excluded_unless" - skipValidationTag = "-" - diveTag = "dive" - keysTag = "keys" - endKeysTag = "endkeys" - requiredTag = "required" - namespaceSeparator = "." - leftBracket = "[" - rightBracket = "]" - restrictedTagChars = ".[],|=+()`~!@#$%^&*\\\"/?<>{}" - restrictedAliasErr = "Alias '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation" - restrictedTagErr = "Tag '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation" + defaultTagName = "validate" + utf8HexComma = "0x2C" + utf8Pipe = "0x7C" + tagSeparator = "," + orSeparator = "|" + tagKeySeparator = "=" + structOnlyTag = "structonly" + noStructLevelTag = "nostructlevel" + omitempty = "omitempty" + omitnil = "omitnil" + isdefault = "isdefault" + requiredWithoutAllTag = "required_without_all" + requiredWithoutTag = "required_without" + requiredWithTag = "required_with" + requiredWithAllTag = "required_with_all" + requiredIfTag = "required_if" + requiredIfContainsTag = "required_if_contains" + requiredUnlessTag = "required_unless" + skipUnlessTag = "skip_unless" + excludedWithoutAllTag = "excluded_without_all" + excludedWithoutTag = "excluded_without" + excludedWithTag = "excluded_with" + excludedWithAllTag = "excluded_with_all" + excludedIfTag = "excluded_if" + excludedIfContainsTag = "excluded_if_contains" + excludedUnlessTag = "excluded_unless" + excludedUnlessContainsTag = "excluded_unless_contains" + skipValidationTag = "-" + diveTag = "dive" + keysTag = "keys" + endKeysTag = "endkeys" + requiredTag = "required" + namespaceSeparator = "." + leftBracket = "[" + rightBracket = "]" + restrictedTagChars = ".[],|=+()`~!@#$%^&*\\\"/?<>{}" + restrictedAliasErr = "Alias '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation" + restrictedTagErr = "Tag '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation" ) var ( @@ -131,8 +132,8 @@ func New(options ...Option) *Validate { switch k { // these require that even if the value is nil that the validation should run, omitempty still overrides this behaviour case requiredIfContainsTag, requiredIfTag, requiredUnlessTag, requiredWithTag, requiredWithAllTag, requiredWithoutTag, requiredWithoutAllTag, - excludedIfContainsTag, excludedIfTag, excludedUnlessTag, excludedWithTag, excludedWithAllTag, excludedWithoutTag, excludedWithoutAllTag, - skipUnlessTag: + excludedIfContainsTag, excludedIfTag, excludedUnlessContainsTag, excludedUnlessTag, excludedWithTag, excludedWithAllTag, excludedWithoutTag, + excludedWithoutAllTag, skipUnlessTag: _ = v.registerValidation(k, wrapFunc(val), true, true) default: // no need to error check here, baked in will always be valid diff --git a/validator_test.go b/validator_test.go index 85e4c4380..f39592f56 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13910,3 +13910,60 @@ func TestExcludeIfContains(t *testing.T) { AssertError(t, errs, "test.FieldTInnerP", "test.FieldTInnerP", "FieldTInnerP", "FieldTInnerP", "excluded_if_contains") AssertError(t, errs, "test.FieldTInner", "test.FieldTInner", "FieldTInner", "FieldTInner", "excluded_if_contains") } + +func TestExcludeUnlessContains(t *testing.T) { + type Inner struct { + Field []string + } + + fieldVal := "test" + type test struct { + InnerP *Inner + Inner Inner + FieldS []string `validate:"omitempty" json:"field_e"` + FieldI []int `validate:"omitempty" json:"field_i"` + FieldIP []*int `validate:"omitempty" json:"field_ip"` + FieldTS string `validate:"excluded_unless_contains=FieldS test" json:"field_ts"` + FieldTI string `validate:"excluded_unless_contains=FieldI 1" json:"field_ti"` + FieldTIP string `validate:"excluded_unless_contains=FieldIP 1" json:"field_tip"` + FieldTInnerP string `validate:"excluded_unless_contains=InnerP.Field test" json:"field_t_innerp"` + FieldTInner string `validate:"excluded_unless_contains=Inner.Field test" json:"field_t_inner"` + } + + validationOk := test{ + InnerP: &Inner{Field: []string{}}, + Inner: Inner{Field: []string{}}, + FieldS: []string{}, + FieldI: []int{}, + } + + validate := New() + + errs := validate.Struct(validationOk) + Equal(t, errs, nil) + + validationNotOk := test{ + InnerP: &Inner{Field: []string{}}, + Inner: Inner{Field: []string{}}, + FieldS: []string{}, + FieldI: []int{}, + FieldIP: []*int{}, + FieldTS: fieldVal, + FieldTI: fieldVal, + FieldTIP: fieldVal, + FieldTInnerP: fieldVal, + FieldTInner: fieldVal, + } + + errs = validate.Struct(validationNotOk) + NotEqual(t, errs, nil) + + ve := errs.(ValidationErrors) + Equal(t, len(ve), 5) + + AssertError(t, errs, "test.FieldTS", "test.FieldTS", "FieldTS", "FieldTS", "excluded_unless_contains") + AssertError(t, errs, "test.FieldTI", "test.FieldTI", "FieldTI", "FieldTI", "excluded_unless_contains") + AssertError(t, errs, "test.FieldTIP", "test.FieldTIP", "FieldTIP", "FieldTIP", "excluded_unless_contains") + AssertError(t, errs, "test.FieldTInnerP", "test.FieldTInnerP", "FieldTInnerP", "FieldTInnerP", "excluded_unless_contains") + AssertError(t, errs, "test.FieldTInner", "test.FieldTInner", "FieldTInner", "FieldTInner", "excluded_unless_contains") +} From fda6e97e7e9c30d678008eddefeeefc2656ac113 Mon Sep 17 00:00:00 2001 From: Lucas Soares Date: Thu, 25 Apr 2024 02:36:21 -0300 Subject: [PATCH 3/3] docs: update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index afbb63a5f..6bf8c534f 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ validate := validator.New(validator.WithRequiredStructEnabled()) | excluded_if | Excluded If | | excluded_if_contains | Excluded If Contains | | excluded_unless | Excluded Unless | +| excluded_unless_contains | Excluded Unless Contains | | excluded_with | Excluded With | | excluded_with_all | Excluded With All | | excluded_without | Excluded Without |