diff --git a/validator.go b/validator.go index e3866f4..71268f0 100644 --- a/validator.go +++ b/validator.go @@ -38,10 +38,11 @@ const ( ) var ( - timeType = reflect.TypeOf(time.Time{}) - timePtrType = reflect.TypeOf(&time.Time{}) - errsPool = &sync.Pool{New: newValidationErrors} - tagsCache = &tagCacheMap{m: map[string][]*tagCache{}} + timeType = reflect.TypeOf(time.Time{}) + timePtrType = reflect.TypeOf(&time.Time{}) + errsPool = &sync.Pool{New: newValidationErrors} + tagsCache = &tagCacheMap{m: map[string][]*tagCache{}} + emptyStructPtr = new(struct{}) ) // returns new ValidationErrors to the pool @@ -180,7 +181,7 @@ func (v *Validate) Field(field interface{}, tag string) ValidationErrors { errs := errsPool.Get().(ValidationErrors) fieldVal := reflect.ValueOf(field) - v.traverseField(fieldVal, fieldVal, fieldVal, "", errs, false, tag, "") + v.traverseField(fieldVal, fieldVal, fieldVal, "", errs, false, tag, "", false, false, nil) if len(errs) == 0 { errsPool.Put(errs) @@ -198,7 +199,91 @@ func (v *Validate) FieldWithValue(val interface{}, field interface{}, tag string errs := errsPool.Get().(ValidationErrors) topVal := reflect.ValueOf(val) - v.traverseField(topVal, topVal, reflect.ValueOf(field), "", errs, false, tag, "") + v.traverseField(topVal, topVal, reflect.ValueOf(field), "", errs, false, tag, "", false, false, nil) + + if len(errs) == 0 { + errsPool.Put(errs) + return nil + } + + return errs +} + +// StructPartial validates the fields that are listed by name in the map including nested structs, unless otherwise specified. Items in the map that are NOT found in the struct will cause a panic. +func (v *Validate) StructPartial(current interface{}, fields ...string) ValidationErrors { + + sv, _ := v.extractType(reflect.ValueOf(current)) + name := sv.Type().Name() + m := map[string]*struct{}{} + + var i int + + if fields != nil { + for _, k := range fields { + + flds := strings.Split(k, ".") + if len(flds) > 0 { + + key := name + for _, s := range flds { + + idx := strings.Index(s, "[") + + if idx != -1 { + for idx != -1 { + i++ + key += s[:idx] + m[key] = emptyStructPtr + + idx2 := strings.Index(s, "]") + idx2++ + key += s[idx:idx2] + m[key] = emptyStructPtr + s = s[idx2:] + idx = strings.Index(s, "[") + + if i == 10 { + idx = -1 + } + } + } else { + + key += s + m[key] = emptyStructPtr + } + + key += "." + } + } + } + } + + errs := errsPool.Get().(ValidationErrors) + + v.tranverseStruct(sv, sv, sv, "", errs, true, len(m) != 0, false, m) + + if len(errs) == 0 { + errsPool.Put(errs) + return nil + } + + return errs +} + +// StructExcept validates the fields in the struct that are NOT listed by name in the map including nested structs, unless otherwise specified. Items in the map that are NOT found in the struct will cause a panic. +func (v *Validate) StructExcept(current interface{}, fields ...string) ValidationErrors { + + sv, _ := v.extractType(reflect.ValueOf(current)) + name := sv.Type().Name() + m := map[string]*struct{}{} + + for _, key := range fields { + m[name+"."+key] = emptyStructPtr + } + + errs := errsPool.Get().(ValidationErrors) + + v.tranverseStruct(sv, sv, sv, "", errs, true, len(m) != 0, true, m) if len(errs) == 0 { errsPool.Put(errs) @@ -214,7 +299,7 @@ func (v *Validate) Struct(current interface{}) ValidationErrors { errs := errsPool.Get().(ValidationErrors) sv := reflect.ValueOf(current) - v.tranverseStruct(sv, sv, sv, "", errs, true) + v.tranverseStruct(sv, sv, sv, "", errs, true, false, false, nil) if len(errs) == 0 { errsPool.Put(errs) @@ -225,7 +310,7 @@ func (v *Validate) Struct(current interface{}) ValidationErrors { } // tranverseStruct traverses a structs fields and then passes them to be validated by traverseField -func (v *Validate) tranverseStruct(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, useStructName bool) { +func (v *Validate) tranverseStruct(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, useStructName bool, partial bool, exclude bool, includeExclude map[string]*struct{}) { if current.Kind() == reflect.Ptr && !current.IsNil() { current = current.Elem() @@ -235,6 +320,7 @@ func (v *Validate) tranverseStruct(topStruct reflect.Value, currentStruct reflec panic("value passed for validation is not a struct") } + var ok bool typ := current.Type() if useStructName { @@ -252,12 +338,21 @@ func (v *Validate) tranverseStruct(topStruct reflect.Value, currentStruct reflec continue } - v.traverseField(topStruct, currentStruct, current.Field(i), errPrefix, errs, true, fld.Tag.Get(v.config.TagName), fld.Name) + if partial { + + _, ok = includeExclude[errPrefix+fld.Name] + + if (ok && exclude) || (!ok && !exclude) { + continue + } + } + + v.traverseField(topStruct, currentStruct, current.Field(i), errPrefix, errs, true, fld.Tag.Get(v.config.TagName), fld.Name, partial, exclude, includeExclude) } } // traverseField validates any field, be it a struct or single field, ensures it's validity and passes it along to be validated via it's tag options -func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, isStructField bool, tag string, name string) { +func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, isStructField bool, tag string, name string, partial bool, exclude bool, includeExclude map[string]*struct{}) { if tag == skipValidationTag { return @@ -319,7 +414,7 @@ func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect. return } - v.tranverseStruct(topStruct, current, current, errPrefix+name+".", errs, false) + v.tranverseStruct(topStruct, current, current, errPrefix+name+".", errs, false, partial, exclude, includeExclude) return } } @@ -402,9 +497,9 @@ func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect. // or panic ;) switch kind { case reflect.Slice, reflect.Array: - v.traverseSlice(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name) + v.traverseSlice(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name, partial, exclude, includeExclude) case reflect.Map: - v.traverseMap(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name) + v.traverseMap(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name, partial, exclude, includeExclude) default: // throw error, if not a slice or map then should not have gotten here // bad dive tag @@ -414,18 +509,18 @@ func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect. } // traverseSlice traverses a Slice or Array's elements and passes them to traverseField for validation -func (v *Validate) traverseSlice(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, tag string, name string) { +func (v *Validate) traverseSlice(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, tag string, name string, partial bool, exclude bool, includeExclude map[string]*struct{}) { for i := 0; i < current.Len(); i++ { - v.traverseField(topStruct, currentStruct, current.Index(i), errPrefix, errs, false, tag, fmt.Sprintf(arrayIndexFieldName, name, i)) + v.traverseField(topStruct, currentStruct, current.Index(i), errPrefix, errs, false, tag, fmt.Sprintf(arrayIndexFieldName, name, i), partial, exclude, includeExclude) } } // traverseMap traverses a map's elements and passes them to traverseField for validation -func (v *Validate) traverseMap(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, tag string, name string) { +func (v *Validate) traverseMap(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, tag string, name string, partial bool, exclude bool, includeExclude map[string]*struct{}) { for _, key := range current.MapKeys() { - v.traverseField(topStruct, currentStruct, current.MapIndex(key), errPrefix, errs, false, tag, fmt.Sprintf(mapIndexFieldName, name, key.Interface())) + v.traverseField(topStruct, currentStruct, current.MapIndex(key), errPrefix, errs, false, tag, fmt.Sprintf(mapIndexFieldName, name, key.Interface()), partial, exclude, includeExclude) } } diff --git a/validator_test.go b/validator_test.go index dce61c5..4cb96cf 100644 --- a/validator_test.go +++ b/validator_test.go @@ -192,6 +192,243 @@ func ValidateValuerType(field reflect.Value) interface{} { return nil } +type TestPartial struct { + NoTag string + BlankTag string `validate:""` + Required string `validate:"required"` + SubSlice []*SubTest `validate:"required,dive"` + Sub *SubTest + SubIgnore *SubTest `validate:"-"` + Anonymous struct { + A string `validate:"required"` + ASubSlice []*SubTest `validate:"required,dive"` + + SubAnonStruct []struct { + Test string `validate:"required"` + OtherTest string `validate:"required"` + } `validate:"required,dive"` + } +} + +func TestStructPartial(t *testing.T) { + + p1 := []string{ + "NoTag", + "Required", + } + + p2 := []string{ + "SubSlice[0].Test", + "Sub", + "SubIgnore", + "Anonymous.A", + } + + p3 := []string{ + "SubTest.Test", + } + + // p4 := []string{ + // "A", + // } + + tPartial := &TestPartial{ + NoTag: "NoTag", + Required: "Required", + + SubSlice: []*SubTest{ + { + + Test: "Required", + }, + { + + Test: "Required", + }, + }, + + Sub: &SubTest{ + Test: "1", + }, + SubIgnore: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + ASubSlice []*SubTest `validate:"required,dive"` + SubAnonStruct []struct { + Test string `validate:"required"` + OtherTest string `validate:"required"` + } `validate:"required,dive"` + }{ + A: "1", + ASubSlice: []*SubTest{ + { + Test: "Required", + }, + { + Test: "Required", + }, + }, + + SubAnonStruct: []struct { + Test string `validate:"required"` + OtherTest string `validate:"required"` + }{ + {"Required", "RequiredOther"}, + {"Required", "RequiredOther"}, + }, + }, + } + + // the following should all return no errors as everything is valid in + // the default state + errs := validate.StructPartial(tPartial, p1...) + Equal(t, errs, nil) + + errs = validate.StructPartial(tPartial, p2...) + Equal(t, errs, nil) + + // this isnt really a robust test, but is ment to illustrate the ANON CASE below + errs = validate.StructPartial(tPartial.SubSlice[0], p3...) + Equal(t, errs, nil) + + errs = validate.StructExcept(tPartial, p1...) + Equal(t, errs, nil) + + errs = validate.StructExcept(tPartial, p2...) + Equal(t, errs, nil) + + // mod tParial for required feild and re-test making sure invalid fields are NOT required: + tPartial.Required = "" + + errs = validate.StructExcept(tPartial, p1...) + Equal(t, errs, nil) + + errs = validate.StructPartial(tPartial, p2...) + Equal(t, errs, nil) + + // inversion and retesting Partial to generate failures: + errs = validate.StructPartial(tPartial, p1...) + NotEqual(t, errs, nil) + AssertError(t, errs, "TestPartial.Required", "Required", "required") + + // errs = validate.StructExcept(tPartial, p2) + // AssertError(t, errs, "TestPartial.Required", "Required", "required") + + // // reset Required field, and set nested struct + // tPartial.Required = "Required" + // tPartial.Anonymous.A = "" + + // // will pass as unset feilds is not going to be tested + // errs = validate.StructPartial(tPartial, p1) + // Equal(t, errs, nil) + + // errs = validate.StructExcept(tPartial, p2) + // Equal(t, errs, nil) + + // // ANON CASE the response here is strange, it clearly does what it is being told to + // errs = validate.StructExcept(tPartial.Anonymous, p4) + // AssertError(t, errs, ".A", "A", "required") + + // // will fail as unset feild is tested + // errs = validate.StructPartial(tPartial, p2) + // AssertError(t, errs, "TestPartial.Anonymous.A", "A", "required") + + // errs = validate.StructExcept(tPartial, p1) + // AssertError(t, errs, "TestPartial.Anonymous.A", "A", "required") + + // // reset nested struct and unset struct in slice + // tPartial.Anonymous.A = "Required" + // tPartial.SubSlice[0].Test = "" + + // // these will pass as unset item is NOT tested + // errs = validate.StructPartial(tPartial, p1) + // Equal(t, errs, nil) + + // errs = validate.StructExcept(tPartial, p2) + // Equal(t, errs, nil) + + // // these will fail as unset item IS tested + // errs = validate.StructExcept(tPartial, p1) + // AssertError(t, errs, "TestPartial.SubSlice[0].Test", "Test", "required") + // Equal(t, len(errs), 1) + + // errs = validate.StructPartial(tPartial, p2) + // //Equal(t, errs, nil) + // AssertError(t, errs, "TestPartial.SubSlice[0].Test", "Test", "required") + // Equal(t, len(errs), 1) + + // // Unset second slice member concurrently to test dive behavior: + // tPartial.SubSlice[1].Test = "" + + // errs = validate.StructPartial(tPartial, p1) + // Equal(t, errs, nil) + + // // Case note: + // // were bypassing dive here? by setting a single item? + // // im not sure anyone would or should do this, I cant think of a reason + // // why they would but you never know. As for describing this behavior in + // // documentation I would be at a loss as to do it + // // especialy concidering the next test + // errs = validate.StructExcept(tPartial, p2) + // Equal(t, errs, nil) + // //AssertError(t, errs, "TestPartial.SubSlice[1].Test", "Test", "required") + + // // test sub validation: + // // this is diving + // errs = validate.StructExcept(tPartial, p1) + // AssertError(t, errs, "TestPartial.SubSlice[0].Test", "Test", "required") + // AssertError(t, errs, "TestPartial.SubSlice[1].Test", "Test", "required") + // Equal(t, len(errs), 2) + + // errs = validate.StructPartial(tPartial, p2) + // //Equal(t, errs, nil) + // AssertError(t, errs, "TestPartial.SubSlice[0].Test", "Test", "required") + // Equal(t, len(errs), 1) + + // // reset struct in slice, and unset struct in slice in unset posistion + // tPartial.SubSlice[0].Test = "Required" + + // // these will pass as the unset item is NOT tested + // errs = validate.StructPartial(tPartial, p1) + // Equal(t, errs, nil) + + // errs = validate.StructPartial(tPartial, p2) + // Equal(t, errs, nil) + + // // testing for missing item by exception, yes it dives and fails + // errs = validate.StructExcept(tPartial, p1) + // AssertError(t, errs, "TestPartial.SubSlice[1].Test", "Test", "required") + // Equal(t, len(errs), 1) + + // // See above case note... this is a variation on the above + // // when all taken into account it seems super strange! + // errs = validate.StructExcept(tPartial, p2) + // Equal(t, errs, nil) + // //AssertError(t, errs, "TestPartial.SubSlice[1].Test", "Test", "required") + + // tPartial.SubSlice[1].Test = "Required" + + // tPartial.Anonymous.SubAnonStruct[0].Test = "" + // // these will pass as the unset item is NOT tested + // errs = validate.StructPartial(tPartial, p1) + // Equal(t, errs, nil) + + // errs = validate.StructPartial(tPartial, p2) + // Equal(t, errs, nil) + + // errs = validate.StructExcept(tPartial, p1) + // AssertError(t, errs, "TestPartial.Anonymous.SubAnonStruct[0].Test", "Test", "required") + + // // See above case note... this is a variation on the above + // // when all taken into account it seems super strange! + // errs = validate.StructExcept(tPartial, p2) + // Equal(t, errs, nil) + // //AssertError(t, errs, "TestPartial.Anonymous.SubAnonStruct[0].Test", "Test", "required") + +} + func TestCrossStructLteFieldValidation(t *testing.T) { type Inner struct {