perform custom validations agains fields containing structs

pull/591/head
Dmitry Zenovich 5 years ago
parent c68441b7f4
commit e02275b630
  1. 129
      validator.go
  2. 54
      validator_test.go

@ -98,6 +98,7 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
var kind reflect.Kind
current, kind, v.fldIsPointer = v.extractTypeInternal(current, false)
var skipValidations, structOnly bool
switch kind {
case reflect.Ptr, reflect.Interface, reflect.Invalid:
@ -165,69 +166,40 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
typ = current.Type()
if typ != timeType {
for tag := ct; tag != nil; tag = tag.next {
switch tag.typeof {
case typeNoStructLevel:
skipValidations = true
case typeStructOnly:
structOnly = true
}
}
if ct != nil {
if ct.typeof == typeStructOnly {
goto CONTINUE
} else if ct.typeof == typeIsDefault {
// set Field Level fields
v.slflParent = parent
v.flField = current
v.cf = cf
v.ct = ct
if !ct.fn(ctx, v) {
v.str1 = string(append(ns, cf.altName...))
if v.v.hasTagNameFunc {
v.str2 = string(append(structNs, cf.name...))
} else {
v.str2 = v.str1
}
v.errs = append(v.errs,
&fieldError{
v: v.v,
tag: ct.aliasTag,
actualTag: ct.tag,
ns: v.str1,
structNs: v.str2,
fieldLen: uint8(len(cf.altName)),
structfieldLen: uint8(len(cf.name)),
value: current.Interface(),
param: ct.param,
kind: kind,
typ: typ,
},
)
return
}
if !structOnly && !skipValidations {
// if len == 0 then validating using 'Var' or 'VarWithValue'
// Var - doesn't make much sense to do it that way, should call 'Struct', but no harm...
// VarWithField - this allows for validating against each field within the struct against a specific value
// pretty handy in certain situations
oldNs := ns
oldStructNs := structNs
if len(cf.name) > 0 {
ns = append(append(ns, cf.altName...), '.')
structNs = append(append(structNs, cf.name...), '.')
}
ct = ct.next
vFldIsPointerOld := v.fldIsPointer
v.validateStruct(ctx, current, current, typ, ns, structNs, ct)
v.fldIsPointer = vFldIsPointerOld
ns = oldNs
structNs = oldStructNs
}
if ct != nil && ct.typeof == typeNoStructLevel {
if skipValidations {
return
}
CONTINUE:
// if len == 0 then validating using 'Var' or 'VarWithValue'
// Var - doesn't make much sense to do it that way, should call 'Struct', but no harm...
// VarWithField - this allows for validating against each field within the struct against a specific value
// pretty handy in certain situations
if len(cf.name) > 0 {
ns = append(append(ns, cf.altName...), '.')
structNs = append(append(structNs, cf.name...), '.')
}
v.validateStruct(ctx, current, current, typ, ns, structNs, ct)
return
}
}
if !ct.hasTag {
if ct != nil && !ct.hasTag {
return
}
@ -435,14 +407,12 @@ OUTER:
}
default:
// set Field Level fields
v.slflParent = parent
v.flField = current
v.cf = cf
v.ct = ct
if !ct.fn(ctx, v) {
if ct.fn != nil {
// set Field Level fields
v.slflParent = parent
v.flField = current
v.cf = cf
v.ct = ct
v.str1 = string(append(ns, cf.altName...))
@ -451,24 +421,25 @@ OUTER:
} else {
v.str2 = v.str1
}
if !ct.fn(ctx, v) {
v.errs = append(v.errs,
&fieldError{
v: v.v,
tag: ct.aliasTag,
actualTag: ct.tag,
ns: v.str1,
structNs: v.str2,
fieldLen: uint8(len(cf.altName)),
structfieldLen: uint8(len(cf.name)),
value: current.Interface(),
param: ct.param,
kind: kind,
typ: typ,
},
)
v.errs = append(v.errs,
&fieldError{
v: v.v,
tag: ct.aliasTag,
actualTag: ct.tag,
ns: v.str1,
structNs: v.str2,
fieldLen: uint8(len(cf.altName)),
structfieldLen: uint8(len(cf.name)),
value: current.Interface(),
param: ct.param,
kind: kind,
typ: typ,
},
)
return
return
}
}
ct = ct.next
}

@ -725,6 +725,36 @@ func TestNilValidator(t *testing.T) {
PanicMatches(t, func() { _ = val.StructPartial(ts, "Test") }, "runtime error: invalid memory address or nil pointer dereference")
}
func TestStructValidation(t *testing.T) {
type inner struct {
Test string `validate:"required"`
}
type TestStruct struct {
Test struct {
Test string `validate:"required"`
} `validate:"one,two,dive"`
Test2 inner `validate:"one,two,dive"`
}
ts := TestStruct{Test2: inner{Test: "some value"}}
val := New()
fn1 := func(fl FieldLevel) bool {
return fl.Field().FieldByName("Test").String() == "some value"
}
fn2 := func(fl FieldLevel) bool {
return fl.Field().FieldByName("Test").String() == "another value"
}
val.RegisterValidation("one", fn1)
val.RegisterValidation("two", fn2)
errs := val.Struct(ts)
Equal(t, len(errs.(ValidationErrors)), 3)
AssertError(t, errs, "TestStruct.Test.Test", "TestStruct.Test.Test", "Test", "Test", "required")
AssertError(t, errs, "TestStruct.Test", "TestStruct.Test", "Test", "Test", "one")
AssertError(t, errs, "TestStruct.Test2", "TestStruct.Test2", "Test2", "Test2", "two")
}
func TestStructPartial(t *testing.T) {
p1 := []string{
"NoTag",
@ -1021,7 +1051,7 @@ func TestCrossStructLteFieldValidation(t *testing.T) {
// this test is for the WARNING about unforeseen validation issues.
errs = validate.VarWithValue(test, now, "ltecsfield")
NotEqual(t, errs, nil)
Equal(t, len(errs.(ValidationErrors)), 6)
Equal(t, len(errs.(ValidationErrors)), 7)
AssertError(t, errs, "Test.CreatedAt", "Test.CreatedAt", "CreatedAt", "CreatedAt", "ltecsfield")
AssertError(t, errs, "Test.String", "Test.String", "String", "String", "ltecsfield")
AssertError(t, errs, "Test.Int", "Test.Int", "Int", "Int", "ltecsfield")
@ -2766,7 +2796,7 @@ func TestInterfaceErrValidation(t *testing.T) {
AssertError(t, errs, "ExternalCMD.Data.Name", "ExternalCMD.Data.Name", "Name", "Name", "required")
type TestMapStructPtr struct {
Errs map[int]interface{} `validate:"gt=0,dive,len=2"`
Errs map[int]interface{} `validate:"gt=0,dive,required"`
}
mip := map[int]interface{}{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}}
@ -2778,7 +2808,7 @@ func TestInterfaceErrValidation(t *testing.T) {
errs = validate.Struct(msp)
NotEqual(t, errs, nil)
Equal(t, len(errs.(ValidationErrors)), 1)
AssertError(t, errs, "TestMapStructPtr.Errs[3]", "TestMapStructPtr.Errs[3]", "Errs[3]", "Errs[3]", "len")
AssertError(t, errs, "TestMapStructPtr.Errs[3]", "TestMapStructPtr.Errs[3]", "Errs[3]", "Errs[3]", "required")
type TestMultiDimensionalStructs struct {
Errs [][]interface{} `validate:"gt=0,dive,dive"`
@ -5334,6 +5364,20 @@ func TestFieldContains(t *testing.T) {
errs = validate.Struct(stringTestMissingField)
NotEqual(t, errs, nil)
AssertError(t, errs, "StringTestMissingField.Foo", "StringTestMissingField.Foo", "Foo", "Foo", "fieldcontains")
type FooIntTest struct {
Foo int `validate:"fieldcontains=Bar"`
Bar string
}
errs = validate.Struct(&FooIntTest{Foo: 10, Bar: "1"})
NotEqual(t, errs, nil)
type BarIntTest struct {
Foo string `validate:"fieldcontains=Bar"`
Bar int
}
errs = validate.Struct(&BarIntTest{Foo: "10", Bar: 1})
NotEqual(t, errs, nil)
}
func TestFieldExcludes(t *testing.T) {
@ -8116,6 +8160,10 @@ func TestIsDefault(t *testing.T) {
NotEqual(t, errs, nil)
fe = errs.(ValidationErrors)[0]
Equal(t, fe.Field(), "String")
Equal(t, fe.Namespace(), "Test2.inner.String")
Equal(t, fe.Tag(), "isdefault")
fe = errs.(ValidationErrors)[1]
Equal(t, fe.Field(), "inner")
Equal(t, fe.Namespace(), "Test2.inner")
Equal(t, fe.Tag(), "isdefault")

Loading…
Cancel
Save