Skip to content

Commit

Permalink
✨ feat: add new validator: optional for resolve the issues #192
Browse files Browse the repository at this point in the history
  • Loading branch information
inhere committed Jan 21, 2024
1 parent a567209 commit 71857e7
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 22 deletions.
78 changes: 62 additions & 16 deletions issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,49 @@ func TestIssues_172(t *testing.T) {
assert.Equal(t, []string{"test.com", "oof.com", "foobar.com"}, f.Domains)
}

// https://github.com/gookit/validate/issues/192
// v1.4.6 change has broken existing behaviour #192
func TestIssues_192(t *testing.T) {
type Request struct {
Main1 *struct {
Child11 int `json:"child11" validate:"required"`
Child12 int `json:"child12" validate:"required"`
} `json:"main1" validate:"optional"` // optional sometimes

Main2 *struct {
Child21 int `json:"child21" validate:"required"`
Child22 int `json:"child22" validate:"required"`
} `json:"main2"`
}

jsonStr := `{
"main1": {
"child12": 2
},
"main2": {
"child21": 5,
"child22": 6
}
}`

req := &Request{}
err := jsonutil.DecodeString(jsonStr, req)
assert.NoErr(t, err)

t.Run("with child data", func(t *testing.T) {
v := validate.Struct(req)
assert.False(t, v.Validate())
assert.StrContains(t, v.Errors.String(), "main1.child11 is required to not be empty")
})

// set main1 = nil, should not validate Main1.child1
t.Run("set main1 to nil", func(t *testing.T) {
req.Main1 = nil // TODO 应该不验证child1字段了
v := validate.Struct(req)
assert.True(t, v.Validate())
})
}

// https://github.com/gookit/validate/issues/206
func TestIssues_206(t *testing.T) {
m := map[string]any{
Expand Down Expand Up @@ -1291,7 +1334,7 @@ func TestIssues_206(t *testing.T) {
assert.False(t, v.Validate())
// fmt.Println(v.Errors)
assert.Len(t, v.Errors, 1)
assert.StrContains(t, v.Errors.String(), "full_url: origins.*.name must be a valid full URL address")
assert.StrContains(t, v.Errors.String(), "origins.*.name must be a valid full URL address")
}

// https://github.com/gookit/validate/issues/209
Expand Down Expand Up @@ -1493,7 +1536,7 @@ func TestIssues_242(t *testing.T) {
assert.False(t, v.Validate())
s := v.Errors.String()
fmt.Println(v.Errors)
assert.StrContains(t, s, "requiredWithoutAll: ID field is required when none of [NewID] are present")
assert.StrContains(t, s, "ID field is required when none of [NewID] are present")
}

// https://github.com/gookit/validate/issues/245
Expand Down Expand Up @@ -1606,45 +1649,48 @@ func TestIssues_250(t *testing.T) {
HappySalary *int `json:"HappySalary" validate:"int|min:10000" message:"The happy salary is larger than 10.000 EUR"`
}

// works: optional
st := &Salary{}
// works: optional + right value
iv1 := 30000
st := &Salary{OkSalary: &iv1}
v := validate.Struct(st)
assert.True(t, v.Validate())

// works
iv := 3
st = &Salary{
MinSalary: &iv,
}
st = &Salary{MinSalary: &iv}
v = validate.Struct(st)
assert.False(t, v.Validate())
assert.ErrMsg(t, v.Errors, "MinSalary: The minimum salary is 10.000 EUR")
assert.ErrMsg(t, v.Errors, "min: The minimum salary is 10.000 EUR")

// works: use zero value
iv2 := 0
st = &Salary{
MinSalary: &iv2,
}
st = &Salary{MinSalary: &iv2}
v = validate.Struct(st)
assert.False(t, v.Validate())
assert.ErrMsg(t, v.Errors, "MinSalary: The minimum salary is 10.000 EUR")
assert.ErrMsg(t, v.Errors, "min: The minimum salary is 10.000 EUR")

// case 2
type A struct {
A1 *int `validate:"int|min:200"`
A2 *int `validate:"int|min:200"`
}

// works
i := 3
// works: optional + right value
i := 300
pa := A{A1: &i, A2: nil}
v = validate.Struct(pa)
assert.True(t, v.Validate())

// works: optional + error value
i2 := 3
pa = A{A1: &i2, A2: nil}
v = validate.Struct(pa)
assert.False(t, v.Validate())
assert.StrContains(t, v.Errors.String(), "A1 min value is 200")

// works
i2 := 0
pa = A{A1: &i2, A2: nil}
i3 := 0
pa = A{A1: &i3, A2: nil}
v = validate.Struct(pa)
assert.False(t, v.Validate())
assert.StrContains(t, v.Errors.String(), "A1 min value is 200")
Expand Down
5 changes: 4 additions & 1 deletion register.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ func init() {
// some commonly validation rule names.
const (
RuleRequired = "required"
RuleRegexp = "regexp"
RuleOptional = "optional"

RuleDefault = "default"
RuleRegexp = "regexp"
// RuleSafe means skip validate this field
RuleSafe = "safe"
RuleSafe1 = "-"
Expand Down
15 changes: 11 additions & 4 deletions rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func (v *Validation) StringRule(field, rule string, filterRule ...string) *Valid
realName := ValidatorName(validator)
switch realName {
// add default value for the field
case "default":
case RuleDefault:
v.SetDefValue(field, list[1])
// eg 'regex:\d{4,6}' dont need split args. args is "\d{4,6}"
case RuleRegexp:
Expand Down Expand Up @@ -260,11 +260,18 @@ func (v *Validation) addOneRule(fields, validator, realName string, args []any)
// init some settings
rule.realName = realName
rule.skipEmpty = v.SkipOnEmpty
// validator name is not "required"
rule.nameNotRequired = !strings.HasPrefix(realName, "required")
rule.optional = realName == RuleOptional
// validator name is not "requiredX"
rule.nameNotRequired = !strings.HasPrefix(realName, RuleRequired)

// append
// append rule
v.rules = append(v.rules, rule)
if rule.optional {
for _, field := range rule.fields {
v.optionals[field] = 0
}
}

return rule
}

Expand Down
3 changes: 2 additions & 1 deletion validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ func newEmpty() *Validation {
// trans: StdTranslator,
trans: NewTranslator(),
// validated data
safeData: make(map[string]any),
safeData: make(map[string]any),
optionals: make(map[string]int8),
// validator names
validators: make(map[string]int8, 16),
// filtered data
Expand Down
34 changes: 34 additions & 0 deletions validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ type Validation struct {

// translator instance
trans *Translator
// optional fields, useful for sub-struct field in struct data. eg: "Parent"
//
// key is field name, value is field vale is: init=0 empty=1 not-empty=2.
optionals map[string]int8
}

// NewEmpty new validation instance, but not with data.
Expand Down Expand Up @@ -137,6 +141,7 @@ func (v *Validation) Reset() {
func (v *Validation) resetRules() {
// reset rules
v.rules = v.rules[:0]
v.optionals = make(map[string]int8)
v.filterRules = v.filterRules[:0]
}

Expand Down Expand Up @@ -582,6 +587,35 @@ func (v *Validation) shouldStop() bool {
return v.hasError && v.StopOnError
}

// check current field is in optional parent field.
//
// return: true - optional parent field value is empty.
func (v *Validation) isInOptional(field string) bool {
for name, flag := range v.optionals {
// check like: field="Parent.Child" name="Parent"
if strings.HasPrefix(field, name+".") {
if flag != 0 {
return flag == 1 // 1=empty
}

pVal, exist, zero := v.tryGet(name)
if !exist || zero {
v.optionals[name] = 1
return true // not check field.
}
if IsEmpty(pVal) {
v.optionals[name] = 1
return true // not check field.
}

v.optionals[name] = 2
return false
}
}

return false
}

func (v *Validation) isNotNeedToCheck(field string) bool {
if len(v.sceneFields) == 0 {
return false
Expand Down
4 changes: 4 additions & 0 deletions validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ func Validators() map[string]int8 {

// Required field val check
func (v *Validation) Required(field string, val any) bool {
if v.isInOptional(field) {
return true
}

if v.data != nil && v.data.Type() == sourceForm {
// check is upload file
if v.data.(*FormData).HasFile(field) {
Expand Down

0 comments on commit 71857e7

Please sign in to comment.