From 80ac4c5d2aba531cca1d03b6f0c2dae4c08da165 Mon Sep 17 00:00:00 2001 From: janik-mac Date: Wed, 21 Feb 2024 16:44:39 -0500 Subject: [PATCH 1/5] Feat: select tag --- cache.go | 18 ++++++++++++++ doc.go | 14 +++++++++++ validator.go | 56 ++++++++++++++++++++++++++++++++++++++++--- validator_instance.go | 1 + validator_test.go | 46 +++++++++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 3 deletions(-) diff --git a/cache.go b/cache.go index b6bdd11a..d7c00d99 100644 --- a/cache.go +++ b/cache.go @@ -21,12 +21,14 @@ const ( typeKeys typeEndKeys typeOmitNil + typeSelect ) const ( invalidValidation = "Invalid validation tag on field '%s'" undefinedValidation = "Undefined validation function '%s' on field '%s'" keysTagNotDefined = "'" + endKeysTag + "' tag encountered without a corresponding '" + keysTag + "' tag" + invalidSelectTag = "'select' tags must have exactly one value" ) type structCache struct { @@ -266,6 +268,22 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s continue default: + if strings.HasPrefix(t, selectTag) { + vals := strings.SplitN(t, tagKeySeparator, 2) + + // Check again for exact match to prevent future conflicts + if vals[0] == selectTag { + if len(vals) == 1 { + panic(invalidSelectTag) + } + + current.typeof = typeSelect + current.hasParam = true + current.param = vals[1] + continue + } + } + if t == isdefault { current.typeof = typeIsDefault } diff --git a/doc.go b/doc.go index b4740918..c360d12d 100644 --- a/doc.go +++ b/doc.go @@ -249,6 +249,20 @@ Example #2 // eq=1|eq=2 will be applied to each array element in the map keys // required will be applied to map values +# Select + +Selects a struct field or map value for which the following tags will be applied. +It is similar to the dive tags for arrays/slices/maps except that it only applies to a single struct field or map value. + + Usage: select=FieldName + +Example: + + // Validates that the field "Int64" of "MyStruct.Field" is greater than 10 when it is non-zero + type MyStruct struct { + Field sql.NullInt64 `validate:"select=Int64,omitempty,gt=10"` + } + # Required This validates that the value is not the data types default zero value. diff --git a/validator.go b/validator.go index 901e7b50..cb3f45c0 100644 --- a/validator.go +++ b/validator.go @@ -39,7 +39,6 @@ func (v *validate) validateStruct(ctx context.Context, parent reflect.Value, cur } if len(ns) == 0 && len(cs.name) != 0 { - ns = append(ns, cs.name...) ns = append(ns, '.') @@ -58,7 +57,6 @@ func (v *validate) validateStruct(ctx context.Context, parent reflect.Value, cur f = cs.fields[i] if v.isPartial { - if v.ffn != nil { // used with StructFiltered if v.ffn(append(structNs, f.name...)) { @@ -445,8 +443,60 @@ OUTER: ct = ct.next } - default: + case typeSelect: + var name, altName string + var fieldValue reflect.Value + switch kind { + case reflect.Struct: + + v.misc = append(v.misc[0:0], cf.name...) + v.misc = append(v.misc, '.') + v.misc = append(v.misc, ct.param...) + name = string(v.misc) + + if cf.namesEqual { + altName = name + } else { + v.misc = append(v.misc[0:0], cf.altName...) + v.misc = append(v.misc, '.') + v.misc = append(v.misc, ct.param...) + altName = string(v.misc) + } + fieldValue = current.FieldByName(ct.param) + + case reflect.Map: + + v.misc = append(v.misc[0:0], cf.name...) + v.misc = append(v.misc, '[') + v.misc = append(v.misc, ct.param...) + v.misc = append(v.misc, ']') + name = string(v.misc) + + if cf.namesEqual { + altName = name + } else { + v.misc = append(v.misc[0:0], cf.altName...) + v.misc = append(v.misc, '[') + v.misc = append(v.misc, ct.param...) + v.misc = append(v.misc, ']') + altName = string(v.misc) + } + + fieldValue = current.MapIndex(reflect.ValueOf(ct.param)) + + default: + panic("can't select field on a non struct or map types") + } + + v.traverseField(ctx, parent, fieldValue, ns, structNs, &cField{ + altName: altName, + name: name, + }, ct.next) + + return + + default: // set Field Level fields v.slflParent = parent v.flField = current diff --git a/validator_instance.go b/validator_instance.go index 1a345138..f2b0d082 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -38,6 +38,7 @@ const ( excludedIfTag = "excluded_if" excludedUnlessTag = "excluded_unless" skipValidationTag = "-" + selectTag = "select" diveTag = "dive" keysTag = "keys" endKeysTag = "endkeys" diff --git a/validator_test.go b/validator_test.go index 3b6d2634..b1ad5de1 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13794,3 +13794,49 @@ func TestPrivateFieldsStruct(t *testing.T) { Equal(t, len(errs), tc.errorNum) } } + +func TestSelectTag(t *testing.T) { + validator := New(WithRequiredStructEnabled()) + + t.Run("on struct", func(t *testing.T) { + type Test struct { + Int sql.NullInt64 `validate:"required,select=Int64,gt=1"` + } + + validCase := Test{sql.NullInt64{Int64: 2}} + zeroCase := Test{} + invalidCase := Test{sql.NullInt64{Int64: 1}} + + Equal(t, validator.Struct(validCase), nil) + AssertError(t, validator.Struct(zeroCase), "Test.Int", "Test.Int", "Int", "Int", "required") + AssertError(t, validator.Struct(invalidCase), "Test.Int.Int64", "Test.Int.Int64", "Int.Int64", "Int.Int64", "gt") + }) + + t.Run("on map", func(t *testing.T) { + type Test struct { + Map map[string]int `validate:"required,select=key,gt=1"` + } + + validCase := Test{map[string]int{"key": 2}} + zeroCase := Test{} + invalidCase := Test{map[string]int{"key": 1}} + + Equal(t, validator.Struct(validCase), nil) + AssertError(t, validator.Struct(zeroCase), "Test.Map", "Test.Map", "Map", "Map", "required") + AssertError(t, validator.Struct(invalidCase), "Test.Map[key]", "Test.Map[key]", "Map[key]", "Map[key]", "gt") + }) + + t.Run("missing select value", func(t *testing.T) { + type Test struct { + Int sql.NullInt64 `validate:"required,select"` + } + + defer func() { + if r := recover(); r != invalidSelectTag { + t.Errorf("Expected panic %q, got %v", invalidSelectTag, r) + } + }() + + _ = validator.Struct(Test{}) + }) +} From 2f18b16f89b6a3a731b600d90349e77e77298cb7 Mon Sep 17 00:00:00 2001 From: janik-mac Date: Wed, 21 Feb 2024 17:13:38 -0500 Subject: [PATCH 2/5] Rollback unnecessary spacing changes --- validator.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/validator.go b/validator.go index cb3f45c0..1981ea84 100644 --- a/validator.go +++ b/validator.go @@ -39,6 +39,7 @@ func (v *validate) validateStruct(ctx context.Context, parent reflect.Value, cur } if len(ns) == 0 && len(cs.name) != 0 { + ns = append(ns, cs.name...) ns = append(ns, '.') @@ -57,6 +58,7 @@ func (v *validate) validateStruct(ctx context.Context, parent reflect.Value, cur f = cs.fields[i] if v.isPartial { + if v.ffn != nil { // used with StructFiltered if v.ffn(append(structNs, f.name...)) { From d2d87cdfcff0e0d49919ff64f1a303b8ea81a9f5 Mon Sep 17 00:00:00 2001 From: janik-mac Date: Thu, 22 Feb 2024 10:39:25 -0500 Subject: [PATCH 3/5] updated docs with a more meaningful example --- doc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc.go b/doc.go index c360d12d..19172e1e 100644 --- a/doc.go +++ b/doc.go @@ -258,9 +258,9 @@ It is similar to the dive tags for arrays/slices/maps except that it only applie Example: - // Validates that the field "Int64" of "MyStruct.Field" is greater than 10 when it is non-zero + // Validates that the field "V" of "MyStruct.Field" is greater than 10 when it is non-zero type MyStruct struct { - Field sql.NullInt64 `validate:"select=Int64,omitempty,gt=10"` + Field sql.Null[uint] `validate:"select=omitempty,V,gt=10"` } # Required From 30e1df09fc81e49b429165ca5ec5dbe2a0cbc038 Mon Sep 17 00:00:00 2001 From: janik-mac Date: Thu, 22 Feb 2024 11:13:52 -0500 Subject: [PATCH 4/5] updated doc example comment to reflect previous change --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index 19172e1e..edbc3fe0 100644 --- a/doc.go +++ b/doc.go @@ -258,7 +258,7 @@ It is similar to the dive tags for arrays/slices/maps except that it only applie Example: - // Validates that the field "V" of "MyStruct.Field" is greater than 10 when it is non-zero + // Validates that the field "V" of "MyStruct.Field" is greater than 10 when "Field" is valid type MyStruct struct { Field sql.Null[uint] `validate:"select=omitempty,V,gt=10"` } From 7eb58afab34a368220d0a7ecbbc9c74e983d6c0b Mon Sep 17 00:00:00 2001 From: janik-mac Date: Thu, 22 Feb 2024 11:15:36 -0500 Subject: [PATCH 5/5] fixed example tag ordering --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index edbc3fe0..98e817d6 100644 --- a/doc.go +++ b/doc.go @@ -260,7 +260,7 @@ Example: // Validates that the field "V" of "MyStruct.Field" is greater than 10 when "Field" is valid type MyStruct struct { - Field sql.Null[uint] `validate:"select=omitempty,V,gt=10"` + Field sql.Null[uint] `validate:"omitempty,select=V,gt=10"` } # Required