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
6 changes: 3 additions & 3 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ TestGen is a separate program under `testgen/`. It is described at the end.

`internal/common` holds the shared `FieldType`, the normalized type names, `KeyPath`, and `CountValues`.

`types` is the package generated validators import, `github.com/opencodeco/validgen/types`. It defines `ValidationError` and the helpers the condition table calls, including `IsValidEmail`, `EqualFold`, `SliceOnlyContains`, `SliceNotContains`, `MapOnlyContains`, and `MapNotContains`.
`types` is the package generated validators import, `github.com/opencodeco/validgen/types`. It defines `ValidationError` and the helpers the condition table calls, including `IsValidEmail`, `SliceOnlyContains`, `SliceNotContains`, `MapOnlyContains`, and `MapNotContains`. Case-insensitive string checks call `strings.EqualFold` directly.

## Parser

Expand Down Expand Up @@ -142,11 +142,11 @@ errs = append(errs, types.NewValidationError("message"))

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}}`.

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 `types.EqualFold`. Field comparisons compile to `obj.Field` compared with `obj.Other` or `obj.Nested.Field`.
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`.

When `IsGoType` is false, each validation on that field appends a nested call instead of a condition-table test. The call is `TypeValidate(&obj.Field)`, where `Type` is `BaseType`. If `BaseType` starts with the struct's own package name and a dot, that prefix is removed. A same-package field whose `BaseType` is `main.InnerStructType` calls `InnerStructTypeValidate`. A field whose `BaseType` is `mypkg.InnerStructType` calls `mypkg.InnerStructTypeValidate`. The call is emitted when `BaseType` is in the parsed-struct index. A missing type returns `no validator found for struct type`.

Imports kept on the generated package are the struct file's imports whose local name is a package name parsed in this run. `buildImportPath` writes each of those paths as a quoted import and always adds `github.com/opencodeco/validgen/types`. When any struct in the package has `UnmarshalJSON` source, it also adds `encoding/json` and `errors`.
Imports kept on the generated package are the struct file's imports whose local name is a package name parsed in this run. `buildImportPath` writes each of those paths as a quoted import and always adds `github.com/opencodeco/validgen/types`. When any struct in the package has `UnmarshalJSON` source, it also adds `encoding/json` and `errors`. When generated code calls `strings.EqualFold`, it also adds `strings`.

## Package writer

Expand Down
8 changes: 4 additions & 4 deletions internal/codegenerator/condition_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,15 +296,15 @@ var conditionTable = map[string]Operation{
{
AcceptedTypes: []string{"<STRING>"},
ConditionTable: ConditionTable{
operation: `types.EqualFold(obj.{{.Name}}, "{{.Target}}")`,
operation: `strings.EqualFold(obj.{{.Name}}, "{{.Target}}")`,
concatOperator: "",
errorMessage: "{{.Name}} must be equal to '{{.Target}}'",
},
},
{
AcceptedTypes: []string{"*<STRING>"},
ConditionTable: ConditionTable{
operation: `obj.{{.Name}} != nil && types.EqualFold(*obj.{{.Name}}, "{{.Target}}")`,
operation: `obj.{{.Name}} != nil && strings.EqualFold(*obj.{{.Name}}, "{{.Target}}")`,
concatOperator: "",
errorMessage: "{{.Name}} must be equal to '{{.Target}}'",
},
Expand Down Expand Up @@ -388,15 +388,15 @@ var conditionTable = map[string]Operation{
{
AcceptedTypes: []string{"<STRING>"},
ConditionTable: ConditionTable{
operation: `!types.EqualFold(obj.{{.Name}}, "{{.Target}}")`,
operation: `!strings.EqualFold(obj.{{.Name}}, "{{.Target}}")`,
concatOperator: "",
errorMessage: "{{.Name}} must not be equal to '{{.Target}}'",
},
},
{
AcceptedTypes: []string{"*<STRING>"},
ConditionTable: ConditionTable{
operation: `obj.{{.Name}} != nil && !types.EqualFold(*obj.{{.Name}}, "{{.Target}}")`,
operation: `obj.{{.Name}} != nil && !strings.EqualFold(*obj.{{.Name}}, "{{.Target}}")`,
concatOperator: "",
errorMessage: "{{.Name}} must not be equal to '{{.Target}}'",
},
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions internal/codegenerator/get_test_elements_all_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ func TestDefineTestElementsWithAllTypes(t *testing.T) {
types: []string{"<STRING>"},
value: "abc",
want: TestElements{
conditions: []string{`types.EqualFold(obj.field, "abc")`},
conditions: []string{`strings.EqualFold(obj.field, "abc")`},
concatOperator: "",
errorMessage: "field must be equal to 'abc'",
},
Expand All @@ -288,7 +288,7 @@ func TestDefineTestElementsWithAllTypes(t *testing.T) {
types: []string{"*<STRING>"},
value: "abc",
want: TestElements{
conditions: []string{`obj.field != nil && types.EqualFold(*obj.field, "abc")`},
conditions: []string{`obj.field != nil && strings.EqualFold(*obj.field, "abc")`},
concatOperator: "",
errorMessage: "field must be equal to 'abc'",
},
Expand All @@ -304,7 +304,7 @@ func TestDefineTestElementsWithAllTypes(t *testing.T) {
types: []string{"<STRING>"},
value: "abc",
want: TestElements{
conditions: []string{`!types.EqualFold(obj.field, "abc")`},
conditions: []string{`!strings.EqualFold(obj.field, "abc")`},
concatOperator: "",
errorMessage: "field must not be equal to 'abc'",
},
Expand All @@ -313,7 +313,7 @@ func TestDefineTestElementsWithAllTypes(t *testing.T) {
types: []string{"*<STRING>"},
value: "abc",
want: TestElements{
conditions: []string{`obj.field != nil && !types.EqualFold(*obj.field, "abc")`},
conditions: []string{`obj.field != nil && !strings.EqualFold(*obj.field, "abc")`},
concatOperator: "",
errorMessage: "field must not be equal to 'abc'",
},
Expand Down
4 changes: 2 additions & 2 deletions internal/codegenerator/get_test_elements_pointer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestDefineTestElementsWithStringPointerFields(t *testing.T) {
{
validation: "eq_ignore_case=AbC",
want: TestElements{
conditions: []string{`obj.Field != nil && types.EqualFold(*obj.Field, "AbC")`},
conditions: []string{`obj.Field != nil && strings.EqualFold(*obj.Field, "AbC")`},
errorMessage: "Field must be equal to 'AbC'",
},
},
Expand All @@ -65,7 +65,7 @@ func TestDefineTestElementsWithStringPointerFields(t *testing.T) {
{
validation: "neq_ignore_case=AbC",
want: TestElements{
conditions: []string{`obj.Field != nil && !types.EqualFold(*obj.Field, "AbC")`},
conditions: []string{`obj.Field != nil && !strings.EqualFold(*obj.Field, "AbC")`},
errorMessage: "Field must not be equal to 'AbC'",
},
},
Expand Down
4 changes: 2 additions & 2 deletions internal/codegenerator/get_test_elements_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestDefineTestElementsWithStringFields(t *testing.T) {
fieldValidation: "eq_ignore_case=AbC",
},
want: TestElements{
conditions: []string{`types.EqualFold(obj.myStrField, "AbC")`},
conditions: []string{`strings.EqualFold(obj.myStrField, "AbC")`},
errorMessage: "myStrField must be equal to 'AbC'",
},
},
Expand Down Expand Up @@ -101,7 +101,7 @@ func TestDefineTestElementsWithStringFields(t *testing.T) {
fieldValidation: "neq_ignore_case=AbC",
},
want: TestElements{
conditions: []string{`!types.EqualFold(obj.MyFieldNotEqual, "AbC")`},
conditions: []string{`!strings.EqualFold(obj.MyFieldNotEqual, "AbC")`},
errorMessage: "MyFieldNotEqual must not be equal to 'AbC'",
},
},
Expand Down
23 changes: 21 additions & 2 deletions internal/pkgwriter/file_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"go/format"
"os"
"strings"
"text/template"

"github.com/opencodeco/validgen/internal/codegenerator"
Expand Down Expand Up @@ -44,10 +45,28 @@ func needsJSONUnmarshal(pkg *codegenerator.Pkg) bool {
return false
}

func codeUsesStringsEqualFold(pkg *codegenerator.Pkg) bool {
const needle = "strings.EqualFold"
for _, st := range pkg.Structs {
if strings.Contains(st.ValidatorFuncCode, needle) || strings.Contains(st.UnmarshalJSONCode, needle) {
return true
}
}
return false
}

func buildImportPath(pkg *codegenerator.Pkg) (string, error) {
code := ""
if needsJSONUnmarshal(pkg) {
code += "\t\"encoding/json\"\n\t\"errors\"\n\n"
needsJSON := needsJSONUnmarshal(pkg)
needsStrings := codeUsesStringsEqualFold(pkg)
if needsJSON {
code += "\t\"encoding/json\"\n\t\"errors\"\n"
}
if needsStrings {
code += "\t\"strings\"\n"
}
if needsJSON || needsStrings {
code += "\n"
}
code += "\t\"github.com/opencodeco/validgen/types\""

Expand Down
51 changes: 51 additions & 0 deletions internal/pkgwriter/file_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,54 @@ func (obj *User) UnmarshalJSON(b []byte) error {
t.Errorf("BuildFileValidatorCode() diff =\n%s", dmp.DiffPrettyText(dmp.DiffMain(want, got, false)))
}
}

func TestBuildFileValidatorImportsStringsForEqualFold(t *testing.T) {
got, err := BuildFileValidatorCode(&codegenerator.Pkg{
Name: "main",
Structs: map[string]*codegenerator.Struct{
"User": {
Struct: &analyzer.Struct{
Struct: parser.Struct{
PackageName: "main",
StructName: "User",
},
},
ValidatorFuncCode: `
func UserValidate(obj *User) []error {
var errs []error
if !(strings.EqualFold(obj.Name, "abc")) {
errs = append(errs, types.NewValidationError("Name must be equal to 'abc'"))
}
return errs
}`,
},
},
})
if err != nil {
t.Fatalf("BuildFileValidatorCode() error = %v", err)
}

want := `// Code generated by ValidGen. DO NOT EDIT.

//nolint:all
package main

import (
"strings"

"github.com/opencodeco/validgen/types"
)

func UserValidate(obj *User) []error {
var errs []error
if !(strings.EqualFold(obj.Name, "abc")) {
errs = append(errs, types.NewValidationError("Name must be equal to 'abc'"))
}
return errs
}
`
if got != want {
dmp := diffmatchpatch.New()
t.Errorf("BuildFileValidatorCode() diff =\n%s", dmp.DiffPrettyText(dmp.DiffMain(want, got, false)))
}
}
10 changes: 6 additions & 4 deletions tests/cmpbenchtests/validator__.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions tests/endtoend/validator__.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading