Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,20 +121,20 @@ The following table shows the validations and possible types, where:

| Validation/Type | String | Numeric types (integers and floats) | Complex | Boolean | Slice | Array | Map | Time | Duration |
| - | - | - | - | - | - | - | - | - | - |
| eq | I | I | W | I | - | - | - | W | W |
| eq | I | I | I | I | - | - | - | W | W |
| eq_ignore_case | I | - | - | - | - | - | - | - | - |
| gt | - | I | - | - | - | - | - | W | W |
| gte | - | I | - | - | - | - | - | W | W |
| lt | - | I | - | - | - | - | - | W | W |
| lte | - | I | - | - | - | - | - | W | W |
| neq | I | I | W | I | - | - | - | W | W |
| neq | I | I | I | I | - | - | - | W | W |
| neq_ignore_case | I | - | - | - | - | - | - | - | - |
| len | I | - | - | - | I | - | W | - | - |
| max | I | - | - | - | I | - | W | W | W |
| min | I | - | - | - | I | - | W | W | W |
| in | I | I | W | - | I | I | W | - | W |
| nin | I | I | W | - | I | I | W | - | W |
| required | I | I | W | - | I | - | W | W | W |
| in | I | I | I | - | I | I | W | - | W |
| nin | I | I | I | - | I | I | W | - | W |
| required | I | I | I | - | I | - | W | W | W |
| email | I | - | - | - | - | - | - | - | - |
| eqfield | I | I | I | I | - | - | - | W | W |
| neqfield | I | I | I | I | - | - | - | W | W |
Expand All @@ -143,7 +143,7 @@ The following table shows the validations and possible types, where:
| ltefield | - | I | - | - | - | - | - | W | W |
| ltfield | - | I | - | - | - | - | - | W | W |

Complex (`complex64`, `complex128`) supports `eqfield` and `neqfield` via Go `==` / `!=`. Ordering field tags (`gtefield`, `gtfield`, `ltefield`, `ltfield`) are rejected: Go has no `<` / `>` / `<=` / `>=` for complex values, and go-playground/validator does not define concrete ordering semantics for them.
Complex (`complex64`, `complex128`) supports `eq`, `neq`, `in`, `nin`, `eqfield`, and `neqfield` via Go `==` / `!=`, and `required` via `!= 0` (the zero value `0+0i`). Tag values are Go imaginary literals without spaces, for example `eq=1+2i` and `in=1+2i 5+6i`. Ordering tags (`gt`, `gte`, `lt`, `lte`, `gtfield`, `gtefield`, `ltfield`, `ltefield`) are rejected: Go has no `<` / `>` / `<=` / `>=` for complex values.

## Steps to run the unit tests

Expand Down
6 changes: 4 additions & 2 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,12 @@ An unknown operation has `CountValues` zero, which is `UndefinedValue`, and `Par

If the field's `BaseType` is `package.Struct` for a struct parsed in this run, the type check stops there. Otherwise the field must be a Go type, and `IsValidByType` must accept the operation for `ToNormalizedString`.

`IsValidByType` strips one leading `*`. For `required` on a pointer it returns true without reading the type list. Every other operation must list the remaining normalized type. `GetConditionTable` still has to find a row for that normalized type when code is generated.
`IsValidByType` strips one leading `*`. A stripped `<COMPLEX>` pointer is rejected, because the condition table has no `*<COMPLEX>` row. For `required` on any other pointer it returns true without reading the type list. Every other operation must list the remaining normalized type. `GetConditionTable` still has to find a row for that normalized type when code is generated.

### Field comparisons

Scalar `<COMPLEX>` accepts `eq`, `neq`, `in`, `nin`, and `required`. The equality rows emit `==` or `!=` against the tag literal. `required` emits `!= 0`, which is the complex zero value. `gt`, `gte`, `lt`, and `lte` stay limited to `<INT>` and `<FLOAT>`, so the generator does not emit `<`, `>`, `<=`, or `>=` for complex values. Slice, array, and map complex values are rejected. Scalar complex pointers are rejected in `IsValidByType` and omitted from generation.

`eqfield`, `neqfield`, `gtfield`, `gtefield`, `ltfield`, and `ltefield` set `IsFieldOperation`. `eqfield` and `neqfield` allow `<STRING>`, `<INT>`, `<FLOAT>`, `<COMPLEX>`, and `<BOOL>`. Their condition-table rows compare with `==` and `!=`. `gtfield`, `gtefield`, `ltfield`, and `ltefield` allow `<INT>` and `<FLOAT>` only. `analyzeFieldOperations` checks field operations after the catalog checks.

The target is the single value from the tag. `Field2` refers to a field of the same struct. `Nested.Field2` refers to a field of the struct stored on `Nested`. The lookup key is `common.KeyPath`, which joins names with `.`. Both fields must already be in the parsed set, and their `FieldType` values must be equal. That comparison includes `ComposedType`, `BaseType`, and `Size`, so `int` and `int32` do not match.
Expand Down Expand Up @@ -140,7 +142,7 @@ errs = append(errs, types.NewValidationError("message"))

`GetConditionTable` selects the row whose `AcceptedTypes` contain `ToNormalizedString`. `DefineTestElements` substitutes placeholders in that row's `operation` string. `{{.Name}}` becomes the field name. `{{.Target}}` becomes one tag value.

Scalar `in` rows set `concatOperator` to `||`, and the per-value copies are joined. That includes `*<STRING>`, `*<INT>`, `*<FLOAT>`, and `*<BOOL>`. Scalar `nin` rows set `concatOperator` to `&&`. Slice, array, and map rows leave `concatOperator` empty, so `DefineTestElements` keeps one copy. That copy lists every target through `{{.TargetsAsStringSlice}}` or `{{.TargetsAsNumericSlice}}`.
Scalar `in` rows set `concatOperator` to `||`, and the per-value copies are joined. That includes `<COMPLEX>`, `*<STRING>`, `*<INT>`, `*<FLOAT>`, and `*<BOOL>`. Scalar `nin` rows set `concatOperator` to `&&`. Complex scalar rows use the same `==` / `!=` form as numeric scalars, with the tag literal left unquoted. Slice, array, and map rows leave `concatOperator` empty, so `DefineTestElements` keeps one copy. That copy lists every target through `{{.TargetsAsStringSlice}}` or `{{.TargetsAsNumericSlice}}`.

Those slice and map copies call `types.SliceOnlyContains`, `types.SliceNotContains`, `types.MapOnlyContains`, or `types.MapNotContains`. Non-pointer array rows pass `obj.Field[:]` into the slice helpers. Literal string comparisons quote the target. `email` calls `types.IsValidEmail`. `eq_ignore_case` and `neq_ignore_case` call `strings.EqualFold`. Field comparisons compile to `obj.Field` compared with `obj.Other` or `obj.Nested.Field`.

Expand Down
80 changes: 80 additions & 0 deletions internal/analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,3 +731,83 @@ func TestAnalyzeStructsWithInvalidNestedFieldOperations(t *testing.T) {
})
}
}

func TestAnalyzeComplexScalarOperations(t *testing.T) {
validTags := []string{
`valid:"required"`,
`valid:"eq=1+2i"`,
`valid:"neq=3-4i"`,
`valid:"in=1+2i 5+6i"`,
`valid:"nin=7+8i 9+0i"`,
}
invalidTags := []struct {
tag string
op string
}{
{tag: `valid:"gt=1+2i"`, op: "gt"},
{tag: `valid:"gte=1+2i"`, op: "gte"},
{tag: `valid:"lt=1+2i"`, op: "lt"},
{tag: `valid:"lte=1+2i"`, op: "lte"},
}

for _, baseType := range []string{"complex64", "complex128"} {
for _, tag := range validTags {
t.Run(baseType+" "+tag, func(t *testing.T) {
_, err := AnalyzeStructs([]*parser.Struct{{
Fields: []parser.Field{{
FieldName: "Value",
Type: common.FieldType{BaseType: baseType},
Tag: tag,
}},
}})
if err != nil {
t.Errorf("AnalyzeStructs() error = %v", err)
}
})
}

for _, tt := range invalidTags {
t.Run(baseType+" "+tt.tag, func(t *testing.T) {
_, err := AnalyzeStructs([]*parser.Struct{{
Fields: []parser.Field{{
FieldName: "Value",
Type: common.FieldType{BaseType: baseType},
Tag: tt.tag,
}},
}})
wantErr := types.NewValidationError("operation %s: invalid %s(<COMPLEX>) type", tt.op, baseType)
if err != wantErr {
t.Errorf("AnalyzeStructs() error = %v, wantErr %v", err, wantErr)
}
})
}
}

composed := []struct {
name string
composed string
norm string
tag string
op string
}{
{name: "slice", composed: "[]", norm: "[]<COMPLEX>", tag: `valid:"eq=1+2i"`, op: "eq"},
{name: "map", composed: "map", norm: "map[<COMPLEX>]", tag: `valid:"eq=1+2i"`, op: "eq"},
{name: "pointer", composed: "*", norm: "*<COMPLEX>", tag: `valid:"eq=1+2i"`, op: "eq"},
{name: "pointer required", composed: "*", norm: "*<COMPLEX>", tag: `valid:"required"`, op: "required"},
}
for _, shape := range composed {
t.Run(shape.name, func(t *testing.T) {
_, err := AnalyzeStructs([]*parser.Struct{{
Fields: []parser.Field{{
FieldName: "Value",
Type: common.FieldType{BaseType: "complex128", ComposedType: shape.composed},
Tag: shape.tag,
}},
}})
wantErr := types.NewValidationError("operation %s: invalid complex128(%s) type", shape.op, shape.norm)
if err != wantErr {
t.Errorf("AnalyzeStructs() error = %v, wantErr %v", err, wantErr)
}
})
}
}
10 changes: 8 additions & 2 deletions internal/analyzer/operations/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,21 @@ func (o *Operations) IsValid(op string) bool {

func (o *Operations) IsValidByType(op, fieldType string) bool {
// * is a modifier and can be ignored for type validation.
fieldType, pointer := strings.CutPrefix(fieldType, "*")
normalized, pointer := strings.CutPrefix(fieldType, "*")

// Scalar complex pointers have no condition-table row. Reject them here
// so analysis does not accept a tag that generation cannot emit.
if pointer && normalized == "<COMPLEX>" {
return false
}

// Required can be used with all pointer types.
if pointer && op == "required" {
// Required can be used with all pointers types.
return true
}

return slices.Contains(o.operations[op].ValidTypes, fieldType)
return slices.Contains(o.operations[op].ValidTypes, normalized)
}

func (o *Operations) IsFieldOperation(op string) bool {
Expand Down
10 changes: 5 additions & 5 deletions internal/analyzer/operations/operations_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ var operationsList = map[string]Operation{
"eq": {
CountValues: common.OneValue,
IsFieldOperation: false,
ValidTypes: []string{"<STRING>", "<INT>", "<FLOAT>", "<BOOL>"},
ValidTypes: []string{"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>", "<BOOL>"},
Comment thread
leocavalcante marked this conversation as resolved.
},
"required": {
CountValues: common.ZeroValue,
IsFieldOperation: false,
ValidTypes: []string{
"<STRING>", "<INT>", "<FLOAT>", "<BOOL>",
"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>", "<BOOL>",
"[]<STRING>", "[]<INT>", "[]<FLOAT>", "[]<BOOL>",
"map[<STRING>]", "map[<INT>]", "map[<FLOAT>]", "map[<BOOL>]"},
},
Expand Down Expand Up @@ -68,7 +68,7 @@ var operationsList = map[string]Operation{
"neq": {
CountValues: common.OneValue,
IsFieldOperation: false,
ValidTypes: []string{"<STRING>", "<BOOL>", "<INT>", "<FLOAT>"},
ValidTypes: []string{"<STRING>", "<BOOL>", "<INT>", "<FLOAT>", "<COMPLEX>"},
},
"neq_ignore_case": {
CountValues: common.OneValue,
Expand All @@ -79,7 +79,7 @@ var operationsList = map[string]Operation{
CountValues: common.ManyValues,
IsFieldOperation: false,
ValidTypes: []string{
"<STRING>", "<INT>", "<FLOAT>", "<BOOL>",
"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>", "<BOOL>",
"[]<STRING>", "[]<INT>", "[]<FLOAT>", "[]<BOOL>",
"[N]<STRING>", "[N]<INT>", "[N]<FLOAT>", "[N]<BOOL>",
"map[<STRING>]", "map[<INT>]", "map[<FLOAT>]", "map[<BOOL>]",
Expand All @@ -89,7 +89,7 @@ var operationsList = map[string]Operation{
CountValues: common.ManyValues,
IsFieldOperation: false,
ValidTypes: []string{
"<STRING>", "<INT>", "<FLOAT>", "<BOOL>",
"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>", "<BOOL>",
"[]<STRING>", "[]<INT>", "[]<FLOAT>", "[]<BOOL>",
"[N]<STRING>", "[N]<INT>", "[N]<FLOAT>", "[N]<BOOL>",
"map[<STRING>]", "map[<INT>]", "map[<FLOAT>]", "map[<BOOL>]",
Expand Down
93 changes: 83 additions & 10 deletions internal/analyzer/operations/operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestOperationsIsValidByType(t *testing.T) {
{
op: "eq",
fieldTypes: []string{
"<STRING>", "<INT>", "<FLOAT>", "<BOOL>",
"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>", "<BOOL>",
"*<STRING>", "*<INT>", "*<FLOAT>", "*<BOOL>",
},
valid: true,
Expand All @@ -69,7 +69,7 @@ func TestOperationsIsValidByType(t *testing.T) {
{
op: "required",
fieldTypes: []string{
"<STRING>", "<INT>", "<FLOAT>",
"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>",
"[]<STRING>", "[]<INT>", "[]<FLOAT>", "[]<BOOL>",
"map[<STRING>]", "map[<INT>]", "map[<FLOAT>]", "map[<BOOL>]",
"*<STRING>", "*<INT>", "*<FLOAT>", "*<BOOL>",
Expand Down Expand Up @@ -172,7 +172,7 @@ func TestOperationsIsValidByType(t *testing.T) {
{
op: "neq",
fieldTypes: []string{
"<STRING>", "<INT>", "<FLOAT>", "<BOOL>",
"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>", "<BOOL>",
"*<STRING>", "*<INT>", "*<FLOAT>", "*<BOOL>",
},
valid: true,
Expand All @@ -192,7 +192,7 @@ func TestOperationsIsValidByType(t *testing.T) {
{
op: "in",
fieldTypes: []string{
"<STRING>", "<INT>", "<FLOAT>", "<BOOL>",
"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>", "<BOOL>",
"[]<STRING>", "[]<INT>", "[]<FLOAT>", "[]<BOOL>",
"map[<STRING>]", "map[<INT>]", "map[<FLOAT>]", "map[<BOOL>]",
"[N]<STRING>", "[N]<INT>", "[N]<FLOAT>", "[N]<BOOL>",
Expand All @@ -208,7 +208,7 @@ func TestOperationsIsValidByType(t *testing.T) {
{
op: "nin",
fieldTypes: []string{
"<STRING>", "<INT>", "<FLOAT>", "<BOOL>",
"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>", "<BOOL>",
"[]<STRING>", "[]<INT>", "[]<FLOAT>", "[]<BOOL>",
"map[<STRING>]", "map[<INT>]", "map[<FLOAT>]", "map[<BOOL>]",
"[N]<STRING>", "[N]<INT>", "[N]<FLOAT>", "[N]<BOOL>",
Expand All @@ -235,7 +235,7 @@ func TestOperationsIsValidByType(t *testing.T) {
op: "eqfield",
fieldTypes: []string{
"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>", "<BOOL>",
"*<STRING>", "*<INT>", "*<FLOAT>", "*<COMPLEX>", "*<BOOL>",
"*<STRING>", "*<INT>", "*<FLOAT>", "*<BOOL>",
},
valid: true,
},
Expand All @@ -245,7 +245,7 @@ func TestOperationsIsValidByType(t *testing.T) {
op: "neqfield",
fieldTypes: []string{
"<STRING>", "<INT>", "<FLOAT>", "<COMPLEX>", "<BOOL>",
"*<STRING>", "*<INT>", "*<FLOAT>", "*<COMPLEX>", "*<BOOL>",
"*<STRING>", "*<INT>", "*<FLOAT>", "*<BOOL>",
},
valid: true,
},
Expand Down Expand Up @@ -306,15 +306,88 @@ func TestOperationsIsValidByType(t *testing.T) {
{
op: "gt",
fieldTypes: []string{
"<BOOL>",
"[]<STRING>", "[]<INT>", "[]<FLOAT>", "[]<BOOL>",
"<BOOL>", "<COMPLEX>",
"[]<STRING>", "[]<INT>", "[]<FLOAT>", "[]<BOOL>", "[]<COMPLEX>",
"map[<STRING>]", "map[<INT>]", "map[<FLOAT>]", "map[<BOOL>]",
"*<BOOL>",
"*<BOOL>", "*<COMPLEX>",
"*[]<STRING>", "*[]<INT>", "*[]<FLOAT>", "*[]<BOOL>",
"*map[<STRING>]", "*map[<INT>]", "*map[<FLOAT>]", "*map[<BOOL>]",
},
valid: false,
},
{
op: "gte",
fieldTypes: []string{
"<STRING>", "<BOOL>", "<COMPLEX>",
"*<COMPLEX>", "[]<COMPLEX>",
},
valid: false,
},
{
op: "lt",
fieldTypes: []string{
"<STRING>", "<BOOL>", "<COMPLEX>",
"*<COMPLEX>", "[]<COMPLEX>",
},
valid: false,
},
{
op: "lte",
fieldTypes: []string{
"<STRING>", "<BOOL>", "<COMPLEX>",
"*<COMPLEX>", "[]<COMPLEX>",
},
valid: false,
},
{
op: "eq",
fieldTypes: []string{
"*<COMPLEX>", "[]<COMPLEX>", "[N]<COMPLEX>", "map[<COMPLEX>]",
},
valid: false,
},
{
op: "required",
fieldTypes: []string{
"*<COMPLEX>",
},
valid: false,
},
{
op: "neq",
fieldTypes: []string{
"*<COMPLEX>", "[]<COMPLEX>", "[N]<COMPLEX>", "map[<COMPLEX>]",
},
valid: false,
},
{
op: "in",
fieldTypes: []string{
"*<COMPLEX>", "[]<COMPLEX>", "[N]<COMPLEX>", "map[<COMPLEX>]",
},
valid: false,
},
{
op: "nin",
fieldTypes: []string{
"*<COMPLEX>", "[]<COMPLEX>", "[N]<COMPLEX>", "map[<COMPLEX>]",
},
valid: false,
},
{
op: "eqfield",
fieldTypes: []string{
"*<COMPLEX>",
},
valid: false,
},
{
op: "neqfield",
fieldTypes: []string{
"*<COMPLEX>",
},
valid: false,
},
{
op: "neq_ignore_case",
fieldTypes: []string{"<INT>", "<FLOAT>", "<BOOL>", "[]<STRING>", "map[<STRING>]", "<XPTO>", "*<XPTO>"},
Expand Down
Loading
Loading