diff --git a/baked_in.go b/baked_in.go index f7e89489..7897ff61 100644 --- a/baked_in.go +++ b/baked_in.go @@ -53,6 +53,7 @@ var ( keysTag: {}, endKeysTag: {}, structOnlyTag: {}, + skipNamespaceTag: {}, omitzero: {}, omitempty: {}, omitnil: {}, diff --git a/cache.go b/cache.go index fb101b06..a9cabc12 100644 --- a/cache.go +++ b/cache.go @@ -22,6 +22,7 @@ const ( typeEndKeys typeOmitNil typeOmitZero + typeSkipNamespace ) const ( @@ -77,11 +78,12 @@ type cStruct struct { } type cField struct { - idx int - name string - altName string - namesEqual bool - cTags *cTag + idx int + name string + altName string + namesEqual bool + skipNamespace bool + cTags *cTag } type cTag struct { @@ -100,6 +102,15 @@ type cTag struct { runValidationWhenNil bool } +func (ct *cTag) HasTagInChain(tag string) bool { + for n := ct; n != nil; n = n.next { + if n.aliasTag == tag { + return true + } + } + return false +} + func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStruct { v.structCache.lock.Lock() defer v.structCache.lock.Unlock() // leave as defer! because if inner panics, it will never get unlocked otherwise! @@ -161,11 +172,12 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr } cs.fields = append(cs.fields, &cField{ - idx: i, - name: fld.Name, - altName: customName, - cTags: ctag, - namesEqual: fld.Name == customName, + idx: i, + name: fld.Name, + altName: customName, + cTags: ctag, + namesEqual: fld.Name == customName, + skipNamespace: ctag.HasTagInChain(skipNamespaceTag), }) } v.structCache.Set(typ, cs) @@ -256,6 +268,9 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s case structOnlyTag: current.typeof = typeStructOnly + case skipNamespaceTag: + current.typeof = typeSkipNamespace + case noStructLevelTag: current.typeof = typeNoStructLevel diff --git a/doc.go b/doc.go index 91bace77..d58098d5 100644 --- a/doc.go +++ b/doc.go @@ -186,6 +186,18 @@ Same as structonly tag except that any struct level validations will not run. Usage: nostructlevel +# Skip Namespace + +Instruct validator to skip name of embedded structure into field namespace. + + Usage: skipns + +Example #1 + + type Outer struct { + Embedded `validate:"skipns"` + } + # Omit Empty Allows conditional validation, for example if a field is not set with diff --git a/validator.go b/validator.go index 2347d4c5..2c857813 100644 --- a/validator.go +++ b/validator.go @@ -192,7 +192,9 @@ OUTER: // 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...), '.') + if !cf.skipNamespace { + ns = append(append(ns, cf.altName...), '.') + } structNs = append(append(structNs, cf.name...), '.') } @@ -205,6 +207,9 @@ OUTER: case typeNoStructLevel: return + case typeSkipNamespace: + ct = ct.next + case typeStructOnly: if isNestedStruct { // if len == 0 then validating using 'Var' or 'VarWithValue' diff --git a/validator_instance.go b/validator_instance.go index 9362cd73..078b14cc 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -21,6 +21,7 @@ const ( tagKeySeparator = "=" structOnlyTag = "structonly" noStructLevelTag = "nostructlevel" + skipNamespaceTag = "skipns" omitzero = "omitzero" omitempty = "omitempty" omitnil = "omitnil" diff --git a/validator_test.go b/validator_test.go index 85284ecc..0be7bdcf 100644 --- a/validator_test.go +++ b/validator_test.go @@ -420,6 +420,63 @@ func TestNameNamespace(t *testing.T) { Equal(t, fe.StructNamespace(), "Namespace.Inner1.Inner2.String[1]") } +func TestEmbeddingAndNamespace(t *testing.T) { + type Embedded struct { + EmbeddedString string `validate:"required" json:"JSONString"` + } + + type OuterWithoutSkipTag struct { + Embedded + } + + type OuterWithSkipTag struct { + Embedded `validate:"skipns"` + } + + validate := New() + validate.RegisterTagNameFunc(func(fld reflect.StructField) string { + name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] + + if name == "-" { + return "" + } + + return name + }) + + // Without skip tag + ts1 := &OuterWithoutSkipTag{ + Embedded{EmbeddedString: "ok"}, + } + + errs := validate.Struct(ts1) + Equal(t, errs, nil) + + ts1.EmbeddedString = "" + + errs = validate.Struct(ts1) + NotEqual(t, errs, nil) + ve := errs.(ValidationErrors) + Equal(t, len(ve), 1) + AssertError(t, errs, "OuterWithoutSkipTag.Embedded.JSONString", "OuterWithoutSkipTag.Embedded.EmbeddedString", "JSONString", "EmbeddedString", "required") + + // Without skip tag + ts2 := &OuterWithSkipTag{ + Embedded{EmbeddedString: "ok"}, + } + + errs = validate.Struct(ts2) + Equal(t, errs, nil) + + ts2.EmbeddedString = "" + + errs = validate.Struct(ts2) + NotEqual(t, errs, nil) + ve = errs.(ValidationErrors) + Equal(t, len(ve), 1) + AssertError(t, errs, "OuterWithSkipTag.JSONString", "OuterWithSkipTag.Embedded.EmbeddedString", "JSONString", "EmbeddedString", "required") +} + func TestAnonymous(t *testing.T) { validate := New() validate.RegisterTagNameFunc(func(fld reflect.StructField) string {