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

Add required_if and required_unless #658

Merged
merged 2 commits into from Sep 27, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -197,6 +197,8 @@ Baked-in Validations
| min | Minimum |
| oneof | One Of |
| required | Required |
| required_if | Required If |
| required_unless | Required Unless |
| required_with | Required With |
| required_with_all | Required With All |
| required_without | Required Without |
Expand Down
59 changes: 59 additions & 0 deletions baked_in.go
Expand Up @@ -64,6 +64,8 @@ var (
// or even disregard and use your own map if so desired.
bakedInValidators = map[string]Func{
"required": hasValue,
"required_if": requiredIf,
"required_unless": requiredUnless,
"required_with": requiredWith,
"required_with_all": requiredWithAll,
"required_without": requiredWithout,
Expand Down Expand Up @@ -1387,6 +1389,63 @@ 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 {
field, kind, _, found := fl.GetStructFieldOKAdvanced2(fl.Parent(), param)
if !found {
return defaultNotFoundValue
}

switch kind {

case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return field.Int() == asInt(value)

case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return field.Uint() == asUint(value)

case reflect.Float32, reflect.Float64:
return field.Float() == asFloat(value)

case reflect.Slice, reflect.Map, reflect.Array:
return int64(field.Len()) == asInt(value)
}

// default reflect.String:
return field.String() == value
}

// requiredIf 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 requiredIf(fl FieldLevel) bool {
params := parseOneOfParam2(fl.Param())
if len(params)%2 != 0 {
panic(fmt.Sprintf("Bad param number for required_if %s", fl.FieldName()))
}
for i := 0; i < len(params); i += 2 {
if !requireCheckFieldValue(fl, params[i], params[i+1], false) {
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 {
params := parseOneOfParam2(fl.Param())
if len(params)%2 != 0 {
panic(fmt.Sprintf("Bad param number for required_unless %s", fl.FieldName()))
}

for i := 0; i < len(params); i += 2 {
if requireCheckFieldValue(fl, params[i], params[i+1], false) {
return true
}
}
return hasValue(fl)
}

// 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 {
Expand Down
34 changes: 34 additions & 0 deletions doc.go
Expand Up @@ -245,6 +245,40 @@ ensures the value is not nil.

Usage: required

Required If

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.

Usage: required_if

Examples:

// require the field if the Field1 is equal to the parameter given:
Usage: required_if=Field1 foobar

// require the field if the Field1 and Field2 is equal to the value respectively:
Usage: required_if=Field1 foo Field2 bar

Required Unless

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.

Usage: required_unless

Examples:

// require the field unless the Field1 is equal to the parameter given:
Usage: required_unless=Field1 foobar

// require the field unless the Field1 and Field2 is equal to the value respectively:
Usage: required_unless=Field1 foo Field2 bar

Required With

The field under validation must be present and not empty only if any
Expand Down
4 changes: 3 additions & 1 deletion validator_instance.go
Expand Up @@ -27,6 +27,8 @@ const (
requiredWithoutTag = "required_without"
requiredWithTag = "required_with"
requiredWithAllTag = "required_with_all"
requiredIfTag = "required_if"
requiredUnlessTag = "required_unless"
skipValidationTag = "-"
diveTag = "dive"
keysTag = "keys"
Expand Down Expand Up @@ -107,7 +109,7 @@ func New() *Validate {

switch k {
// these require that even if the value is nil that the validation should run, omitempty still overrides this behaviour
case requiredWithTag, requiredWithAllTag, requiredWithoutTag, requiredWithoutAllTag:
case requiredIfTag, requiredUnlessTag, requiredWithTag, requiredWithAllTag, requiredWithoutTag, requiredWithoutAllTag:
_ = v.registerValidation(k, wrapFunc(val), true, true)
default:
// no need to error check here, baked in will always be valid
Expand Down
145 changes: 145 additions & 0 deletions validator_test.go
Expand Up @@ -8695,6 +8695,151 @@ func TestEndsWithValidation(t *testing.T) {
}
}

func TestRequiredIf(t *testing.T) {
type Inner struct {
Field *string
}

fieldVal := "test"
test := struct {
Inner *Inner
FieldE string `validate:"omitempty" json:"field_e"`
FieldER string `validate:"required_if=FieldE test" json:"field_er"`
Field1 string `validate:"omitempty" json:"field_1"`
Field2 *string `validate:"required_if=Field1 test" json:"field_2"`
Field3 map[string]string `validate:"required_if=Field2 test" json:"field_3"`
Field4 interface{} `validate:"required_if=Field3 1" json:"field_4"`
Field5 int `validate:"required_if=Inner.Field test" json:"field_5"`
Field6 uint `validate:"required_if=Field5 1" json:"field_6"`
Field7 float32 `validate:"required_if=Field6 1" json:"field_7"`
Field8 float64 `validate:"required_if=Field7 1.0" json:"field_8"`
}{
Inner: &Inner{Field: &fieldVal},
Field2: &fieldVal,
Field3: map[string]string{"key": "val"},
Field4: "test",
Field5: 2,
}

validate := New()

errs := validate.Struct(test)
Equal(t, errs, nil)

test2 := struct {
Inner *Inner
Inner2 *Inner
FieldE string `validate:"omitempty" json:"field_e"`
FieldER string `validate:"required_if=FieldE test" json:"field_er"`
Field1 string `validate:"omitempty" json:"field_1"`
Field2 *string `validate:"required_if=Field1 test" json:"field_2"`
Field3 map[string]string `validate:"required_if=Field2 test" json:"field_3"`
Field4 interface{} `validate:"required_if=Field2 test" json:"field_4"`
Field5 string `validate:"required_if=Field3 1" json:"field_5"`
Field6 string `validate:"required_if=Inner.Field test" json:"field_6"`
Field7 string `validate:"required_if=Inner2.Field test" json:"field_7"`
}{
Inner: &Inner{Field: &fieldVal},
Field2: &fieldVal,
}

errs = validate.Struct(test2)
NotEqual(t, errs, nil)

ve := errs.(ValidationErrors)
Equal(t, len(ve), 3)
AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_if")
AssertError(t, errs, "Field4", "Field4", "Field4", "Field4", "required_if")
AssertError(t, errs, "Field6", "Field6", "Field6", "Field6", "required_if")

defer func() {
if r := recover(); r == nil {
t.Errorf("test3 should have panicked!")
}
}()

test3 := struct {
Inner *Inner
Field1 string `validate:"required_if=Inner.Field" json:"field_1"`
}{
Inner: &Inner{Field: &fieldVal},
}
_ = validate.Struct(test3)
}

func TestRequiredUnless(t *testing.T) {
type Inner struct {
Field *string
}

fieldVal := "test"
test := struct {
Inner *Inner
FieldE string `validate:"omitempty" json:"field_e"`
FieldER string `validate:"required_unless=FieldE test" json:"field_er"`
Field1 string `validate:"omitempty" json:"field_1"`
Field2 *string `validate:"required_unless=Field1 test" json:"field_2"`
Field3 map[string]string `validate:"required_unless=Field2 test" json:"field_3"`
Field4 interface{} `validate:"required_unless=Field3 1" json:"field_4"`
Field5 int `validate:"required_unless=Inner.Field test" json:"field_5"`
Field6 uint `validate:"required_unless=Field5 2" json:"field_6"`
Field7 float32 `validate:"required_unless=Field6 0" json:"field_7"`
Field8 float64 `validate:"required_unless=Field7 0.0" json:"field_8"`
}{
FieldE: "test",
Field2: &fieldVal,
Field3: map[string]string{"key": "val"},
Field4: "test",
Field5: 2,
}

validate := New()

errs := validate.Struct(test)
Equal(t, errs, nil)

test2 := struct {
Inner *Inner
Inner2 *Inner
FieldE string `validate:"omitempty" json:"field_e"`
FieldER string `validate:"required_unless=FieldE test" json:"field_er"`
Field1 string `validate:"omitempty" json:"field_1"`
Field2 *string `validate:"required_unless=Field1 test" json:"field_2"`
Field3 map[string]string `validate:"required_unless=Field2 test" json:"field_3"`
Field4 interface{} `validate:"required_unless=Field2 test" json:"field_4"`
Field5 string `validate:"required_unless=Field3 0" json:"field_5"`
Field6 string `validate:"required_unless=Inner.Field test" json:"field_6"`
Field7 string `validate:"required_unless=Inner2.Field test" json:"field_7"`
}{
Inner: &Inner{Field: &fieldVal},
FieldE: "test",
Field1: "test",
}

errs = validate.Struct(test2)
NotEqual(t, errs, nil)

ve := errs.(ValidationErrors)
Equal(t, len(ve), 3)
AssertError(t, errs, "Field3", "Field3", "Field3", "Field3", "required_unless")
AssertError(t, errs, "Field4", "Field4", "Field4", "Field4", "required_unless")
AssertError(t, errs, "Field7", "Field7", "Field7", "Field7", "required_unless")

defer func() {
if r := recover(); r == nil {
t.Errorf("test3 should have panicked!")
}
}()

test3 := struct {
Inner *Inner
Field1 string `validate:"required_unless=Inner.Field" json:"field_1"`
}{
Inner: &Inner{Field: &fieldVal},
}
_ = validate.Struct(test3)
}

func TestRequiredWith(t *testing.T) {
type Inner struct {
Field *string
Expand Down