From 4b554cae62b57be19be2438d4db9becf047ea34b Mon Sep 17 00:00:00 2001 From: Leo Cavalcante Date: Tue, 29 Sep 2026 18:24:09 -0300 Subject: [PATCH] Add the dive tag so validation walks collection elements. Tags after dive apply to each slice, array, or map value, including nested structs the generator already knows. keys and endkeys stay unsupported. --- README.md | 14 ++ docs/internals.md | 8 +- internal/analyzer/analyzer.go | 56 ++++-- internal/analyzer/dive_test.go | 183 ++++++++++++++++++ .../analyzer/operations/operations_list.go | 6 + .../analyzer/operations/operations_test.go | 3 + internal/analyzer/parser_validation_test.go | 19 ++ internal/codegenerator/build_validator.go | 116 +++++++++-- internal/codegenerator/dive_test.go | 140 ++++++++++++++ .../get_test_elements_all_types_test.go | 2 +- internal/codegenerator/test_elements.go | 13 +- internal/common/dive.go | 151 +++++++++++++++ internal/common/dive_test.go | 89 +++++++++ internal/common/field_type.go | 5 + internal/parser/dive_test.go | 61 ++++++ internal/parser/parser.go | 22 ++- internal/parser/parser_test.go | 25 ++- tests/endtoend/dive.go | 65 +++++++ tests/endtoend/main.go | 1 + tests/endtoend/validator__.go | 37 ++++ 20 files changed, 974 insertions(+), 42 deletions(-) create mode 100644 internal/analyzer/dive_test.go create mode 100644 internal/codegenerator/dive_test.go create mode 100644 internal/common/dive.go create mode 100644 internal/common/dive_test.go create mode 100644 internal/parser/dive_test.go create mode 100644 tests/endtoend/dive.go diff --git a/README.md b/README.md index d6bdcf2..8a54366 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,20 @@ The following validations will be implemented: - gtfield (greater than field): field must be greater than another field - ltefield (less than or equal field): field must be less than or equal to another field - ltfield (less than field): field must be less than another field +- dive: tags after `dive` apply to each slice element, array element, or map value + +### dive + +Tags before `dive` apply to the collection. Tags after `dive` apply to each element. A struct element is validated with that struct's generated function. Without `dive`, a slice or map is checked only as a collection, including a slice of structs. + +```go +type User struct { + Addresses []*Address `valid:"required,dive,required"` + Labels map[string]string `valid:"dive,required"` +} +``` + +`required` before `dive` checks that `Addresses` is not empty. After `dive`, each pointer must be non-nil and `Address` field tags run. Each `Labels` value must be non-empty. Map keys are not validated. `keys` and `endkeys` are not implemented. The following table shows the validations and possible types, where: diff --git a/docs/internals.md b/docs/internals.md index b5c896e..ec864da 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -48,9 +48,9 @@ An `*ast.StructType` node fills that struct and appends it. The field loop keeps A pointer appends `*` and then walks the inner expression. A slice walks the element and then appends `[]`. An array walks the element, then sets `Size` from the length literal and appends `[N]`. A map appends `map` and then walks `ast.MapType.Key`. -The map value expression is ignored. Parser tests record `map[string]uint8` with `BaseType` `string`, and `map[uint8]string` with `BaseType` `uint8`. Later stages classify the map from that key type. `FieldType.ToType` prints a map as `map[BaseType]BaseType`. +The map value is stored on `FieldType.MapValue`. Parser tests record `map[string]uint8` with `BaseType` `string` and `MapValue` `uint8`, and `map[uint8]string` with `BaseType` `uint8` and `MapValue` `string`. Collection checks still classify the map from the key type. `FieldType.ToType` still prints a map as `map[BaseType]BaseType`. `dive` reads `MapValue`. -The same walk collapses a pointer element into the container marker. The pointer parser test records `[]*int64` as `BaseType` `int64` and `ComposedType` `*[]`, and `*map[string]bool` as `BaseType` `string` and `ComposedType` `*map`. +The same walk records a pointer on an element separately from a pointer to the container. `[]*int64` is `BaseType` `int64`, `ComposedType` `*[]`, and `ElemPointer` true. `*[]int64` is the same `ComposedType` with `ElemPointer` false. `*map[string]bool` is `BaseType` `string` and `ComposedType` `*map`. A star that remains glued to another container after one `dive`, such as the element of `[][]*string`, is rejected. Nested arrays are rejected because only the outer length is kept. `FieldType.IsGoType` is true when `ComposedType` is `map` or `*map`. Otherwise it is true only when `BaseType` is `string`, `bool`, `float32`, `float64`, `complex64`, `complex128`, or one of the integer names listed in the analyzer section. @@ -144,7 +144,9 @@ Scalar `in` rows set `concatOperator` to `||`, and the per-value copies are join 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`. -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`. +When the field is a struct or a pointer to a struct, 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`. + +`dive` splits the tag list. Tags before `dive` use the field type. Tags after `dive` use `FieldType.DiveInto`, which is one slice element, one array element, or the map value. The generator writes a `for` loop. A struct element calls the nested validator once. A pointer element also checks `required` as non-nil and skips a nil pointer. `required`, `min`, `max`, and `len` on a slice or map whose element is not a Go type use the length check for `[]string` or `map[string]string`. The same length tags apply to `[]*T`. `in` and `nin` do not, because those helpers expect `[]T`. Field comparisons after `dive`, `keys`, `endkeys`, nested arrays, and a pointer glued to another container are rejected. 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`. diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 211865c..a3ddd0e 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -89,27 +89,26 @@ func checkForInvalidOperations(structs []*Struct) error { for _, st := range structs { for i, fd := range st.Fields { + current := fd.Type + dived := false for _, val := range st.FieldsValidations[i].Validations { - // Check if is a valid operation. op := val.Operation if !ops.IsValid(op) { return types.NewValidationError("unsupported operation %s", op) } - // If is a custom struct, check if it has validations. - fdType := fd.Type - if structsWithValidation[fdType.BaseType] { + if op == "dive" { + next, err := current.DiveInto() + if err != nil { + return types.NewValidationError("operation dive: field %s: %s", fd.FieldName, err.Error()) + } + current = next + dived = true continue } - // If has a validation, must be for a go type. - if !fdType.IsGoType() { - return types.NewValidationError("unsupported operation %s with unknown go type %s", op, fdType.BaseType) - } - - // Check if is a valid operation for this type. - if !ops.IsValidByType(op, fdType.ToNormalizedString()) { - return types.NewValidationError("operation %s: invalid %s(%s) type", op, fdType.BaseType, fdType.ToNormalizedString()) + if err := validateOperation(ops, op, current, structsWithValidation, dived); err != nil { + return err } } } @@ -118,6 +117,39 @@ func checkForInvalidOperations(structs []*Struct) error { return nil } +func validateOperation(ops *operations.Operations, op string, ft common.FieldType, structs map[string]bool, dived bool) error { + if dived && ops.IsFieldOperation(op) { + return types.NewValidationError("operation %s: field comparisons are not supported after dive", op) + } + if ft.ElemPointer && !common.IsLenOperation(op) { + return types.NewValidationError("operation %s: cannot apply to a slice or array of pointers", op) + } + + if ft.IsNestedStruct() { + if !structs[ft.BaseType] { + return types.NewValidationError("unsupported operation %s with unknown go type %s", op, ft.BaseType) + } + if dived && op != "required" { + return types.NewValidationError("operation %s: cannot apply to struct %s", op, ft.BaseType) + } + return nil + } + + accept := func(candidate common.FieldType) bool { + return ops.IsValidByType(op, candidate.ToNormalizedString()) + } + if _, ok := ft.OperationType(op, accept); ok { + return nil + } + + lookup := ft.ForCatalog() + if !lookup.IsGoType() { + return types.NewValidationError("unsupported operation %s with unknown go type %s", op, ft.BaseType) + } + + return types.NewValidationError("operation %s: invalid %s(%s) type", op, lookup.BaseType, lookup.ToNormalizedString()) +} + func analyzeFieldOperations(structs []*Struct) error { // Map all fields and their types. diff --git a/internal/analyzer/dive_test.go b/internal/analyzer/dive_test.go new file mode 100644 index 0000000..54fbd7d --- /dev/null +++ b/internal/analyzer/dive_test.go @@ -0,0 +1,183 @@ +package analyzer + +import ( + "testing" + + "github.com/opencodeco/validgen/internal/common" + "github.com/opencodeco/validgen/internal/parser" + "github.com/opencodeco/validgen/types" +) + +func addressStruct() *parser.Struct { + return &parser.Struct{ + PackageName: "main", + StructName: "Address", + Fields: []parser.Field{ + { + FieldName: "Street", + Type: common.FieldType{BaseType: "string"}, + Tag: `valid:"required"`, + }, + }, + } +} + +func TestAnalyzeDiveAccepted(t *testing.T) { + tests := []struct { + name string + field parser.Field + }{ + { + name: "slice of structs", + field: parser.Field{ + FieldName: "Addresses", + Type: common.FieldType{BaseType: "main.Address", ComposedType: "[]"}, + Tag: `valid:"required,dive"`, + }, + }, + { + name: "slice of struct pointers", + field: parser.Field{ + FieldName: "Addresses", + Type: common.FieldType{BaseType: "main.Address", ComposedType: "*[]", ElemPointer: true}, + Tag: `valid:"required,dive,required"`, + }, + }, + { + name: "map value", + field: parser.Field{ + FieldName: "Labels", + Type: common.FieldType{ + BaseType: "string", + ComposedType: "map", + MapValue: &common.FieldType{BaseType: "string"}, + }, + Tag: `valid:"dive,required"`, + }, + }, + { + name: "slice of structs does not dive", + field: parser.Field{ + FieldName: "Addresses", + Type: common.FieldType{BaseType: "main.Address", ComposedType: "[]"}, + Tag: `valid:"required"`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + user := &parser.Struct{ + PackageName: "main", + StructName: "User", + Fields: []parser.Field{tt.field}, + } + if _, err := AnalyzeStructs([]*parser.Struct{addressStruct(), user}); err != nil { + t.Fatalf("AnalyzeStructs() error = %v", err) + } + }) + } +} + +func TestAnalyzeDiveRejected(t *testing.T) { + tests := []struct { + name string + field parser.Field + wantErr error + }{ + { + name: "dive on a string", + field: parser.Field{ + FieldName: "Name", + Type: common.FieldType{BaseType: "string"}, + Tag: `valid:"dive"`, + }, + wantErr: types.NewValidationError("operation dive: field Name: string is not a slice, array, or map"), + }, + { + name: "email on a map value of int", + field: parser.Field{ + FieldName: "Counts", + Type: common.FieldType{ + BaseType: "string", + ComposedType: "map", + MapValue: &common.FieldType{BaseType: "int"}, + }, + Tag: `valid:"dive,email"`, + }, + wantErr: types.NewValidationError("operation email: invalid int() type"), + }, + { + name: "field comparison after dive", + field: parser.Field{ + FieldName: "Names", + Type: common.FieldType{BaseType: "string", ComposedType: "[]"}, + Tag: `valid:"dive,eqfield=Name"`, + }, + wantErr: types.NewValidationError("operation eqfield: field comparisons are not supported after dive"), + }, + { + name: "nested arrays", + field: parser.Field{ + FieldName: "Matrix", + Type: common.FieldType{BaseType: "int", ComposedType: "[N][N]", Size: "2"}, + Tag: `valid:"dive"`, + }, + wantErr: types.NewValidationError("operation dive: field Matrix: nested arrays are not supported"), + }, + { + name: "pointer composition the generator cannot express", + field: parser.Field{ + FieldName: "Matrix", + Type: common.FieldType{BaseType: "string", ComposedType: "*[][]", ElemPointer: true}, + Tag: `valid:"dive"`, + }, + wantErr: types.NewValidationError("operation dive: field Matrix: unsupported pointer composition *[]string"), + }, + { + name: "keys stays unsupported", + field: parser.Field{ + FieldName: "Labels", + Type: common.FieldType{ + BaseType: "string", + ComposedType: "map", + MapValue: &common.FieldType{BaseType: "string"}, + }, + Tag: `valid:"dive,keys,required"`, + }, + wantErr: types.NewValidationError("parser validation keys: unsupported validation keys"), + }, + { + name: "in on a slice of pointers", + field: parser.Field{ + FieldName: "Names", + Type: common.FieldType{BaseType: "string", ComposedType: "*[]", ElemPointer: true}, + Tag: `valid:"in=a b"`, + }, + wantErr: types.NewValidationError("operation in: cannot apply to a slice or array of pointers"), + }, + { + name: "tag other than required on a struct element", + field: parser.Field{ + FieldName: "Addresses", + Type: common.FieldType{BaseType: "main.Address", ComposedType: "[]"}, + Tag: `valid:"dive,email"`, + }, + wantErr: types.NewValidationError("operation email: cannot apply to struct main.Address"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + user := &parser.Struct{ + PackageName: "main", + StructName: "User", + Fields: []parser.Field{tt.field}, + } + _, err := AnalyzeStructs([]*parser.Struct{addressStruct(), user}) + if err != tt.wantErr { + t.Fatalf("AnalyzeStructs() error = %v, want %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/analyzer/operations/operations_list.go b/internal/analyzer/operations/operations_list.go index e9da1a4..55f08a3 100644 --- a/internal/analyzer/operations/operations_list.go +++ b/internal/analyzer/operations/operations_list.go @@ -130,4 +130,10 @@ var operationsList = map[string]Operation{ IsFieldOperation: true, ValidTypes: []string{"", ""}, }, + // dive is a level separator. The analyzer checks the container, then the element. + "dive": { + CountValues: common.ZeroValue, + IsFieldOperation: false, + ValidTypes: nil, + }, } diff --git a/internal/analyzer/operations/operations_test.go b/internal/analyzer/operations/operations_test.go index fe3710e..a10e742 100644 --- a/internal/analyzer/operations/operations_test.go +++ b/internal/analyzer/operations/operations_test.go @@ -34,6 +34,7 @@ func TestOperationsIsValid(t *testing.T) { {op: "gtfield", want: true}, {op: "ltefield", want: true}, {op: "ltfield", want: true}, + {op: "dive", want: true}, {op: "invalid_op", want: false}, } @@ -425,6 +426,7 @@ func TestOperationsIsFieldOperation(t *testing.T) { {op: "gtfield", want: true}, {op: "ltefield", want: true}, {op: "ltfield", want: true}, + {op: "dive", want: false}, {op: "invalid_op", want: false}, } @@ -465,6 +467,7 @@ func TestOperationsArgsCount(t *testing.T) { {op: "gtfield", want: common.OneValue}, {op: "ltefield", want: common.OneValue}, {op: "ltfield", want: common.OneValue}, + {op: "dive", want: common.ZeroValue}, {op: "invalid_op", want: common.UndefinedValue}, } diff --git a/internal/analyzer/parser_validation_test.go b/internal/analyzer/parser_validation_test.go index 606ba5c..59cdaaf 100644 --- a/internal/analyzer/parser_validation_test.go +++ b/internal/analyzer/parser_validation_test.go @@ -132,6 +132,15 @@ func TestValidParserValidation(t *testing.T) { Values: []string{"Nested.field123"}, }, }, + { + name: "dive tag", + validation: "dive", + want: &Validation{ + Operation: "dive", + ExpectedValues: common.ZeroValue, + Values: []string{}, + }, + }, } for _, tt := range tests { @@ -175,6 +184,16 @@ func TestParserInvalidValidation(t *testing.T) { validation: "xpto=a", expectedErr: types.NewValidationError("unsupported validation xpto"), }, + { + name: "keys is not implemented", + validation: "keys", + expectedErr: types.NewValidationError("unsupported validation keys"), + }, + { + name: "endkeys is not implemented", + validation: "endkeys", + expectedErr: types.NewValidationError("unsupported validation endkeys"), + }, { name: "malformed value", validation: "in='abc", diff --git a/internal/codegenerator/build_validator.go b/internal/codegenerator/build_validator.go index 144619e..8a2d492 100644 --- a/internal/codegenerator/build_validator.go +++ b/internal/codegenerator/build_validator.go @@ -66,32 +66,120 @@ func (gv *GenValidations) BuildUnmarshalJSONCode() string { } func (gv *GenValidations) BuildValidationCode(fieldName string, fieldType common.FieldType, fieldValidations []*analyzer.Validation) (string, error) { + return gv.emitValidations("obj."+fieldName, fieldName, fieldType, fieldValidations, false, 0) +} +func (gv *GenValidations) emitValidations(expr, fieldName string, fieldType common.FieldType, fieldValidations []*analyzer.Validation, dived bool, depth int) (string, error) { tests := "" - for _, fieldValidation := range fieldValidations { - var testCode = "" - var err error - - if fieldType.IsGoType() { - testCode, err = gv.buildIfCode(fieldName, fieldType, fieldValidation) - if err != nil { - return "", err - } - } else { - testCode, err = gv.buildIfNestedCode(fieldName, fieldType) + for i, fieldValidation := range fieldValidations { + if fieldValidation.Operation == "dive" { + loop, err := gv.emitDive(expr, fieldName, fieldType, fieldValidations[i+1:], depth) if err != nil { return "", err } + return tests + loop, nil } + testCode, err := gv.emitOne(expr, fieldName, fieldType, fieldValidation, dived) + if err != nil { + return "", err + } tests += testCode } + if dived && fieldType.IsNestedStruct() { + nested, err := gv.emitStructCall(expr, fieldType) + if err != nil { + return "", err + } + tests += nested + } + return tests, nil } -func (gv *GenValidations) buildIfCode(fieldName string, fieldType common.FieldType, fieldValidation *analyzer.Validation) (string, error) { - testElements, err := DefineTestElements(fieldName, fieldType, fieldValidation) +func (gv *GenValidations) emitDive(expr, fieldName string, fieldType common.FieldType, rest []*analyzer.Validation, depth int) (string, error) { + elemType, err := fieldType.DiveInto() + if err != nil { + return "", fmt.Errorf("field %s: %w", fieldName, err) + } + + depth++ + elemExpr := fmt.Sprintf("elem%d", depth) + body, err := gv.emitValidations(elemExpr, fieldName, elemType, rest, true, depth) + if err != nil { + return "", err + } + if strings.TrimSpace(body) == "" { + return "", nil + } + + rangeExpr := expr + prefix := "" + suffix := "" + if fieldType.PointerToContainer() { + rangeExpr = "*" + expr + prefix = fmt.Sprintf("if %s != nil {\n", expr) + suffix = "}\n" + } + + return prefix + fmt.Sprintf("for _, %s := range %s {\n%s}\n", elemExpr, rangeExpr, body) + suffix, nil +} + +func (gv *GenValidations) emitOne(expr, fieldName string, fieldType common.FieldType, fieldValidation *analyzer.Validation, dived bool) (string, error) { + if fieldType.IsNestedStruct() { + if !dived { + return gv.buildIfNestedCode(fieldName, fieldType) + } + if fieldValidation.Operation == "required" && fieldType.ComposedType == "*" { + return fmt.Sprintf("if !(%s != nil) {\nerrs = append(errs, types.NewValidationError(\"%s is required\"))\n}\n", expr, fieldName), nil + } + if fieldValidation.Operation == "required" { + return "", nil + } + return "", fmt.Errorf("operation %s: cannot apply to struct %s", fieldValidation.Operation, fieldType.BaseType) + } + + accept := func(candidate common.FieldType) bool { + _, err := GetConditionTable(fieldValidation.Operation, candidate) + return err == nil + } + target, ok := fieldType.OperationType(fieldValidation.Operation, accept) + if !ok { + return "", fmt.Errorf("field %s: operation %s is not supported for %s", fieldName, fieldValidation.Operation, fieldType.ToType()) + } + + return gv.buildIfCode(expr, fieldName, target, fieldValidation) +} + +func (gv *GenValidations) emitStructCall(expr string, fieldType common.FieldType) (string, error) { + _, ok := gv.StructsWithValidation[fieldType.BaseType] + if !ok { + return "", fmt.Errorf("no validator found for struct type %s", fieldType.ToType()) + } + + funcName := nestedFuncName(gv.Struct.PackageName, fieldType.BaseType) + if fieldType.ComposedType == "*" { + return fmt.Sprintf("if %s != nil {\nerrs = append(errs, %s(%s)...)\n}\n", expr, funcName, expr), nil + } + if fieldType.ComposedType != "" { + return "", fmt.Errorf("no validator found for struct type %s", fieldType.ToType()) + } + + return fmt.Sprintf("errs = append(errs, %s(&%s)...)\n", funcName, expr), nil +} + +func nestedFuncName(packageName, baseType string) string { + pkg := common.ExtractPackage(baseType) + if pkg == packageName { + baseType = strings.TrimPrefix(baseType, pkg+".") + } + + return baseType + "Validate" +} + +func (gv *GenValidations) buildIfCode(expr, fieldName string, fieldType common.FieldType, fieldValidation *analyzer.Validation) (string, error) { + testElements, err := defineTestElements(expr, fieldName, fieldType, fieldValidation) if err != nil { return "", fmt.Errorf("field %s: %w", fieldName, err) } @@ -115,7 +203,7 @@ errs = append(errs, types.NewValidationError("%s")) func (gv *GenValidations) buildIfNestedCode(fieldName string, fieldType common.FieldType) (string, error) { _, ok := gv.StructsWithValidation[fieldType.BaseType] if !ok { - return "", fmt.Errorf("no validator found for struct type %s", fieldType) + return "", fmt.Errorf("no validator found for struct type %s", fieldType.ToType()) } pkg := common.ExtractPackage(fieldType.BaseType) diff --git a/internal/codegenerator/dive_test.go b/internal/codegenerator/dive_test.go new file mode 100644 index 0000000..609c7d8 --- /dev/null +++ b/internal/codegenerator/dive_test.go @@ -0,0 +1,140 @@ +package codegenerator + +import ( + "testing" + + "github.com/opencodeco/validgen/internal/analyzer" + "github.com/opencodeco/validgen/internal/common" + "github.com/opencodeco/validgen/internal/parser" +) + +func TestBuildDiveValidationCode(t *testing.T) { + address := common.FieldType{BaseType: "main.Address"} + tests := []struct { + name string + fieldName string + fieldType common.FieldType + fieldValidation string + want string + }{ + { + name: "slice of structs", + fieldName: "Addresses", + fieldType: common.FieldType{BaseType: "main.Address", ComposedType: "[]"}, + fieldValidation: "dive", + want: `for _, elem1 := range obj.Addresses { +errs = append(errs, AddressValidate(&elem1)...) +} +`, + }, + { + name: "slice of struct pointers", + fieldName: "Addresses", + fieldType: common.FieldType{BaseType: "main.Address", ComposedType: "*[]", ElemPointer: true}, + fieldValidation: "required,dive,required", + want: `if !(len(obj.Addresses) != 0) { +errs = append(errs, types.NewValidationError("Addresses must not be empty")) +} +for _, elem1 := range obj.Addresses { +if !(elem1 != nil) { +errs = append(errs, types.NewValidationError("Addresses is required")) +} +if elem1 != nil { +errs = append(errs, AddressValidate(elem1)...) +} +} +`, + }, + { + name: "map value", + fieldName: "Labels", + fieldType: common.FieldType{ + BaseType: "string", + ComposedType: "map", + MapValue: &common.FieldType{BaseType: "string"}, + }, + fieldValidation: "dive,required", + want: `for _, elem1 := range obj.Labels { +if !(elem1 != "") { +errs = append(errs, types.NewValidationError("Labels is required")) +} +} +`, + }, + { + name: "slice of structs does not dive", + fieldName: "Addresses", + fieldType: common.FieldType{BaseType: "main.Address", ComposedType: "[]"}, + fieldValidation: "required", + want: `if !(len(obj.Addresses) != 0) { +errs = append(errs, types.NewValidationError("Addresses must not be empty")) +} +`, + }, + { + name: "pointer to a slice of structs", + fieldName: "Addresses", + fieldType: common.FieldType{BaseType: "main.Address", ComposedType: "*[]"}, + fieldValidation: "dive", + want: `if obj.Addresses != nil { +for _, elem1 := range *obj.Addresses { +errs = append(errs, AddressValidate(&elem1)...) +} +} +`, + }, + { + name: "nested string slice", + fieldName: "Matrix", + fieldType: common.FieldType{BaseType: "string", ComposedType: "[][]"}, + fieldValidation: "dive,dive,required", + want: `for _, elem1 := range obj.Matrix { +for _, elem2 := range elem1 { +if !(elem2 != "") { +errs = append(errs, types.NewValidationError("Matrix is required")) +} +} +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gv := GenValidations{ + Struct: &analyzer.Struct{ + Struct: parser.Struct{PackageName: "main"}, + }, + StructsWithValidation: map[string]struct{}{ + address.BaseType: {}, + }, + } + var validations []*analyzer.Validation + for _, part := range splitValidations(tt.fieldValidation) { + validations = append(validations, AssertParserValidation(t, part)) + } + got, err := gv.BuildValidationCode(tt.fieldName, tt.fieldType, validations) + if err != nil { + t.Fatalf("BuildValidationCode() error = %v", err) + } + if got != tt.want { + t.Fatalf("BuildValidationCode() = %q, want %q", got, tt.want) + } + }) + } +} + +func splitValidations(tag string) []string { + if tag == "" { + return nil + } + parts := []string{} + start := 0 + for i := 0; i <= len(tag); i++ { + if i == len(tag) || tag[i] == ',' { + parts = append(parts, tag[start:i]) + start = i + 1 + } + } + return parts +} diff --git a/internal/codegenerator/get_test_elements_all_types_test.go b/internal/codegenerator/get_test_elements_all_types_test.go index ccb2982..f750aca 100644 --- a/internal/codegenerator/get_test_elements_all_types_test.go +++ b/internal/codegenerator/get_test_elements_all_types_test.go @@ -1162,7 +1162,7 @@ func TestDefineTestElementsWithAllTypes(t *testing.T) { want.conditions[i] = strings.ReplaceAll(want.conditions[i], "{{.BaseType}}", fieldType.BaseType) } - testName := fmt.Sprintf("validation(%s) type(%s)", validation, fieldType) + testName := fmt.Sprintf("validation(%s) type(%s)", validation, fieldType.ToType()) t.Run(testName, func(t *testing.T) { validation := AssertParserValidation(t, validation) got, err := DefineTestElements("field", fieldType, validation) diff --git a/internal/codegenerator/test_elements.go b/internal/codegenerator/test_elements.go index 5a7f735..9a37b60 100644 --- a/internal/codegenerator/test_elements.go +++ b/internal/codegenerator/test_elements.go @@ -14,6 +14,10 @@ type TestElements struct { } func DefineTestElements(fieldName string, fieldType common.FieldType, fieldValidation *analyzer.Validation) (TestElements, error) { + return defineTestElements("obj."+fieldName, fieldName, fieldType, fieldValidation) +} + +func defineTestElements(expr, fieldName string, fieldType common.FieldType, fieldValidation *analyzer.Validation) (TestElements, error) { condition, err := GetConditionTable(fieldValidation.Operation, fieldType) if err != nil { @@ -27,14 +31,14 @@ func DefineTestElements(fieldName string, fieldType common.FieldType, fieldValid switch fieldValidation.ExpectedValues { case common.ZeroValue: // REFACTOR: codegenerator should inform how many values are expected - roperands = append(roperands, replaceNameAndTarget(condition.operation, fieldName, "")) + roperands = append(roperands, replaceExprNameAndTarget(condition.operation, expr, fieldName, "")) targetValue = condition.operation targetValues = "'" + condition.operation + "' " case common.OneValue, common.ManyValues: valuesAsNumericSlice, valuesAsStringSlice := normalizeSlicesAsCode(fieldType.BaseType, values) for _, value := range values { - operation := replaceNameAndTarget(condition.operation, fieldName, value) + operation := replaceExprNameAndTarget(condition.operation, expr, fieldName, value) operation = replaceSlicesTargets(operation, valuesAsStringSlice, valuesAsNumericSlice) roperands = append(roperands, operation) targetValue = value @@ -49,7 +53,7 @@ func DefineTestElements(fieldName string, fieldType common.FieldType, fieldValid targetValues = strings.TrimSpace(targetValues) errorMsg := condition.errorMessage - errorMsg = replaceNameAndTarget(errorMsg, fieldName, targetValue) + errorMsg = replaceExprNameAndTarget(errorMsg, expr, fieldName, targetValue) errorMsg = replaceTargetInErrors(errorMsg, targetValue, targetValues) return TestElements{ @@ -59,7 +63,8 @@ func DefineTestElements(fieldName string, fieldType common.FieldType, fieldValid }, nil } -func replaceNameAndTarget(text, name, target string) string { +func replaceExprNameAndTarget(text, expr, name, target string) string { + text = strings.ReplaceAll(text, "obj.{{.Name}}", expr) text = strings.ReplaceAll(text, "{{.Name}}", name) text = strings.ReplaceAll(text, "{{.Target}}", target) diff --git a/internal/common/dive.go b/internal/common/dive.go new file mode 100644 index 0000000..e2f91f4 --- /dev/null +++ b/internal/common/dive.go @@ -0,0 +1,151 @@ +package common + +import ( + "fmt" + "strings" +) + +// IsLenOperation reports tags that only check the length of a string, slice, or map. +func IsLenOperation(op string) bool { + switch op { + case "required", "min", "max", "len": + return true + default: + return false + } +} + +// IsNestedStruct reports a struct or pointer to a struct. +// Go scalars and containers are not nested structs. +func (ft FieldType) IsNestedStruct() bool { + if ft.ComposedType != "" && ft.ComposedType != "*" { + return false + } + + return ft.BaseType != "" && !ft.IsGoType() +} + +// PointerToContainer reports *[]T, *[N]T, and *map[K]V. +// []*T keeps the star on the element and returns false. +func (ft FieldType) PointerToContainer() bool { + if ft.ElemPointer || !strings.HasPrefix(ft.ComposedType, "*") { + return false + } + + ct := strings.TrimPrefix(ft.ComposedType, "*") + return isContainerBody(ct) +} + +// ForCatalog drops an element pointer so []*T is checked as a slice of T. +// *[]T is left unchanged. +func (ft FieldType) ForCatalog() FieldType { + if !ft.ElemPointer { + return ft + } + + ft.ElemPointer = false + ft.ComposedType = strings.Replace(ft.ComposedType, "*", "", 1) + return ft +} + +// LenStandIn is a slice or map of string used when a length tag does not +// depend on the element type, such as []Address or [][]string. +func (ft FieldType) LenStandIn() (FieldType, bool) { + ct := ft.ComposedType + ptr := "" + if strings.HasPrefix(ct, "*") && !ft.ElemPointer { + ptr = "*" + ct = strings.TrimPrefix(ct, "*") + } else if ft.ElemPointer { + ct = strings.Replace(ct, "*", "", 1) + } + + switch { + case strings.HasSuffix(ct, "[]"): + return FieldType{ComposedType: ptr + "[]", BaseType: "string"}, true + case ct == "map": + return FieldType{ComposedType: ptr + "map", BaseType: "string"}, true + default: + return FieldType{}, false + } +} + +// OperationType selects the field type used to look up op. +// accept reports whether that type supports the operation. +// in and nin on []*T are rejected: those helpers expect []T, not pointers. +// Length tags only call len, so they still apply to []*T. +func (ft FieldType) OperationType(op string, accept func(FieldType) bool) (FieldType, bool) { + if ft.ElemPointer && !IsLenOperation(op) { + return FieldType{}, false + } + + lookup := ft.ForCatalog() + if knownComposed(lookup.ComposedType) && lookup.IsGoType() && accept(lookup) { + return lookup, true + } + + if IsLenOperation(op) { + if standIn, ok := ft.LenStandIn(); ok && accept(standIn) { + return standIn, true + } + } + + return FieldType{}, false +} + +// DiveInto returns the type of one slice element, array element, or map value. +func (ft FieldType) DiveInto() (FieldType, error) { + ct := ft.ComposedType + if strings.HasPrefix(ct, "*") && !ft.ElemPointer { + ct = strings.TrimPrefix(ct, "*") + } + + switch { + case strings.HasSuffix(ct, "[]"): + return finishDiveElement(FieldType{ + BaseType: ft.BaseType, + ComposedType: strings.TrimSuffix(ct, "[]"), + MapValue: ft.MapValue, + }) + case strings.HasSuffix(ct, "[N]"): + elem := FieldType{ + BaseType: ft.BaseType, + ComposedType: strings.TrimSuffix(ct, "[N]"), + MapValue: ft.MapValue, + } + if strings.Contains(elem.ComposedType, "[N]") { + return FieldType{}, fmt.Errorf("nested arrays are not supported") + } + return finishDiveElement(elem) + case ct == "map": + if ft.MapValue == nil { + return FieldType{}, fmt.Errorf("map value type is not available") + } + return *ft.MapValue, nil + default: + return FieldType{}, fmt.Errorf("%s is not a slice, array, or map", ft.ToType()) + } +} + +func finishDiveElement(elem FieldType) (FieldType, error) { + // A star glued to another container (*[]T versus []*T) cannot be recovered + // once both levels share one ComposedType string. + if strings.Contains(elem.ComposedType, "*") && elem.ComposedType != "*" { + return FieldType{}, fmt.Errorf("unsupported pointer composition %s%s", elem.ComposedType, elem.BaseType) + } + + return elem, nil +} + +func knownComposed(ct string) bool { + switch ct { + case "", "*", "[]", "[N]", "map", "*[]", "*[N]", "*map": + return true + default: + return false + } +} + +func isContainerBody(ct string) bool { + return strings.HasSuffix(ct, "[]") || strings.HasSuffix(ct, "[N]") || ct == "map" || strings.HasSuffix(ct, "map") +} diff --git a/internal/common/dive_test.go b/internal/common/dive_test.go new file mode 100644 index 0000000..5b9f994 --- /dev/null +++ b/internal/common/dive_test.go @@ -0,0 +1,89 @@ +package common + +import "testing" + +func TestDiveInto(t *testing.T) { + tests := []struct { + name string + in FieldType + want FieldType + wantErr string + }{ + { + name: "slice of structs", + in: FieldType{BaseType: "main.Address", ComposedType: "[]"}, + want: FieldType{BaseType: "main.Address"}, + }, + { + name: "slice of pointers", + in: FieldType{BaseType: "main.Address", ComposedType: "*[]", ElemPointer: true}, + want: FieldType{BaseType: "main.Address", ComposedType: "*"}, + }, + { + name: "pointer to slice", + in: FieldType{BaseType: "main.Address", ComposedType: "*[]"}, + want: FieldType{BaseType: "main.Address"}, + }, + { + name: "map value", + in: FieldType{ + BaseType: "string", + ComposedType: "map", + MapValue: &FieldType{BaseType: "string"}, + }, + want: FieldType{BaseType: "string"}, + }, + { + name: "map of structs", + in: FieldType{ + BaseType: "string", + ComposedType: "map", + MapValue: &FieldType{BaseType: "main.Address"}, + }, + want: FieldType{BaseType: "main.Address"}, + }, + { + name: "nested slice", + in: FieldType{BaseType: "string", ComposedType: "[][]"}, + want: FieldType{BaseType: "string", ComposedType: "[]"}, + }, + { + name: "scalar", + in: FieldType{BaseType: "string"}, + wantErr: "string is not a slice, array, or map", + }, + { + name: "nested array", + in: FieldType{BaseType: "int", ComposedType: "[N][N]", Size: "2"}, + wantErr: "nested arrays are not supported", + }, + { + name: "pointer mixed into the next container", + in: FieldType{BaseType: "string", ComposedType: "*[][]", ElemPointer: true}, + wantErr: "unsupported pointer composition *[]string", + }, + { + name: "map without a value type", + in: FieldType{BaseType: "string", ComposedType: "map"}, + wantErr: "map value type is not available", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.in.DiveInto() + if tt.wantErr != "" { + if err == nil || err.Error() != tt.wantErr { + t.Fatalf("DiveInto() error = %v, want %s", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("DiveInto() error = %v", err) + } + if got.BaseType != tt.want.BaseType || got.ComposedType != tt.want.ComposedType || got.ElemPointer != tt.want.ElemPointer { + t.Fatalf("DiveInto() = %#v, want %#v", got, tt.want) + } + }) + } +} diff --git a/internal/common/field_type.go b/internal/common/field_type.go index 8df1d81..8b5887c 100644 --- a/internal/common/field_type.go +++ b/internal/common/field_type.go @@ -11,6 +11,11 @@ type FieldType struct { ComposedType string // array ([N]), map (map) or slice ([]) BaseType string // base type (e.g. string, int, etc.) Size string // for arrays + // MapValue is the value type of a map. Collection checks still use BaseType as the key. + MapValue *FieldType + // ElemPointer is true when a slice or array element is a pointer ([]*T, [N]*T). + // A leading * on ComposedType without ElemPointer is a pointer to the container (*[]T, *map[K]V). + ElemPointer bool } func (ft FieldType) IsGoType() bool { diff --git a/internal/parser/dive_test.go b/internal/parser/dive_test.go new file mode 100644 index 0000000..4841ecf --- /dev/null +++ b/internal/parser/dive_test.go @@ -0,0 +1,61 @@ +package parser + +import ( + "reflect" + "testing" + + "github.com/opencodeco/validgen/internal/common" +) + +func TestParseDiveFieldTypes(t *testing.T) { + src := "package main\n" + + "type User struct {\n" + + " Addresses []*Address `valid:\"required,dive,required\"`\n" + + " Labels map[string]string `valid:\"dive,required\"`\n" + + " Homes []Address `valid:\"required\"`\n" + + " Maybe *[]Address `valid:\"dive\"`\n" + + "}\n" + + "type Address struct {\n" + + " Street string `valid:\"required\"`\n" + + "}\n" + + got, err := parseStructs("example/main.go", src) + if err != nil { + t.Fatal(err) + } + if len(got) != 2 { + t.Fatalf("struct count = %d, want 2", len(got)) + } + + stringValue := &common.FieldType{BaseType: "string"} + want := []Field{ + { + FieldName: "Addresses", + Type: common.FieldType{BaseType: "main.Address", ComposedType: "*[]", ElemPointer: true}, + Tag: `valid:"required,dive,required"`, + }, + { + FieldName: "Labels", + Type: common.FieldType{BaseType: "string", ComposedType: "map", MapValue: stringValue}, + Tag: `valid:"dive,required"`, + }, + { + FieldName: "Homes", + Type: common.FieldType{BaseType: "main.Address", ComposedType: "[]"}, + Tag: `valid:"required"`, + }, + { + FieldName: "Maybe", + Type: common.FieldType{BaseType: "main.Address", ComposedType: "*[]"}, + Tag: `valid:"dive"`, + }, + } + if !reflect.DeepEqual(got[0].Fields, want) { + t.Fatalf("fields mismatch\ngot:\n%swant:\n%s", formatStructs(got[:1]), formatFields(want)) + } +} + +func formatFields(fields []Field) string { + s := &Struct{Fields: fields} + return formatStructs([]*Struct{s}) +} diff --git a/internal/parser/parser.go b/internal/parser/parser.go index e0b22e1..2e772b6 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -192,11 +192,21 @@ func extractCompleteType(fType common.FieldType, expr ast.Expr, packageName stri } return fType, nil case *ast.ArrayType: - // Slice or array type + // Slice or array type. + // A star gained while parsing the element belongs to the element ([]*T), + // not to an outer pointer (*[]T). + before := fType.ComposedType + elemPointerBefore := fType.ElemPointer fType, err = extractCompleteType(fType, v.Elt, packageName) if err != nil { return common.FieldType{}, err } + gained := strings.TrimPrefix(fType.ComposedType, before) + if strings.Contains(gained, "*") { + fType.ElemPointer = true + } else { + fType.ElemPointer = elemPointerBefore + } fType.Size = "" if v.Len != nil { @@ -224,13 +234,21 @@ func extractCompleteType(fType common.FieldType, expr ast.Expr, packageName stri return fType, nil case *ast.MapType: - // Map type + // Map type. BaseType stays the key. The value is kept for dive. fType.ComposedType += "map" fType, err = extractCompleteType(fType, v.Key, packageName) if err != nil { return common.FieldType{}, err } + valueType, err := extractCompleteType(common.FieldType{}, v.Value, packageName) + if err != nil { + return common.FieldType{}, err + } + if valueType.BaseType != "" { + fType.MapValue = &valueType + } + return fType, nil case *ast.StarExpr: // Pointer type diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index b91d1bf..975529a 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -1,6 +1,7 @@ package parser import ( + "fmt" "reflect" "testing" @@ -376,17 +377,17 @@ func TestParseStructsOk(t *testing.T) { Fields: []Field{ { FieldName: "MapField1", - Type: common.FieldType{BaseType: "string", ComposedType: "map", Size: ""}, + Type: common.FieldType{BaseType: "string", ComposedType: "map", Size: "", MapValue: &common.FieldType{BaseType: "string"}}, Tag: "valid:\"required\"", }, { FieldName: "MapField2", - Type: common.FieldType{BaseType: "string", ComposedType: "map", Size: ""}, + Type: common.FieldType{BaseType: "string", ComposedType: "map", Size: "", MapValue: &common.FieldType{BaseType: "uint8"}}, Tag: "valid:\"len=3\"", }, { FieldName: "MapField3", - Type: common.FieldType{BaseType: "uint8", ComposedType: "map", Size: ""}, + Type: common.FieldType{BaseType: "uint8", ComposedType: "map", Size: "", MapValue: &common.FieldType{BaseType: "string"}}, Tag: "valid:\"max=5\"", }, }, @@ -546,17 +547,17 @@ func TestParseStructsOk(t *testing.T) { }, { FieldName: "MapPointer", - Type: common.FieldType{BaseType: "string", ComposedType: "*map", Size: ""}, + Type: common.FieldType{BaseType: "string", ComposedType: "*map", Size: "", MapValue: &common.FieldType{BaseType: "bool"}}, Tag: "valid:\"min=2\"", }, { FieldName: "SliceIntPointer", - Type: common.FieldType{BaseType: "int64", ComposedType: "*[]", Size: ""}, + Type: common.FieldType{BaseType: "int64", ComposedType: "*[]", Size: "", ElemPointer: true}, Tag: "valid:\"min=2\"", }, { FieldName: "ArrayIntPointer", - Type: common.FieldType{BaseType: "int64", ComposedType: "*[5]", Size: ""}, + Type: common.FieldType{BaseType: "int64", ComposedType: "*[N]", Size: "5", ElemPointer: true}, Tag: "valid:\"max=4\"", }, }, @@ -575,6 +576,7 @@ func TestParseStructsOk(t *testing.T) { return } if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseStructs() mismatch\ngot:\n%swant:\n%s", formatStructs(got), formatStructs(tt.want)) gotStr := structsToString(got) wantStr := structsToString(tt.want) dmp := diffmatchpatch.New() @@ -587,6 +589,17 @@ func TestParseStructsOk(t *testing.T) { } } +func formatStructs(structs []*Struct) string { + var result string + for _, s := range structs { + result += fmt.Sprintf("Struct: %s package %s\n", s.StructName, s.PackageName) + for _, f := range s.Fields { + result += fmt.Sprintf(" %s %#v tag %s\n", f.FieldName, f.Type, f.Tag) + } + } + return result +} + func structsToString(structs []*Struct) string { var result string for _, s := range structs { diff --git a/tests/endtoend/dive.go b/tests/endtoend/dive.go new file mode 100644 index 0000000..7dca52c --- /dev/null +++ b/tests/endtoend/dive.go @@ -0,0 +1,65 @@ +package main + +import "log" + +type DiveUser struct { + Addresses []Address `valid:"required,dive"` + Labels map[string]string `valid:"dive,required"` +} + +type ShallowDiveUser struct { + Addresses []Address `valid:"required"` +} + +type PointerDiveUser struct { + Addresses []*Address `valid:"required,dive,required"` +} + +func diveTests() { + log.Println("starting dive tests") + + shallowEmpty := &ShallowDiveUser{} + assertExpectedErrorMsgs("shallow empty", ShallowDiveUserValidate(shallowEmpty), []string{ + "Addresses must not be empty", + }) + + shallowPresent := &ShallowDiveUser{Addresses: []Address{{}}} + assertExpectedErrorMsgs("shallow does not dive", ShallowDiveUserValidate(shallowPresent), nil) + + missing := &DiveUser{ + Addresses: []Address{{}}, + Labels: map[string]string{"home": ""}, + } + assertExpectedErrorMsgs("dive struct and map value", DiveUserValidate(missing), []string{ + "Street is required", + "City is required", + "Labels is required", + }) + + ok := &DiveUser{ + Addresses: []Address{{Street: "av 123", City: "city 123"}}, + Labels: map[string]string{"home": "earth"}, + } + assertExpectedErrorMsgs("dive valid", DiveUserValidate(ok), nil) + + nilElem := &PointerDiveUser{Addresses: []*Address{nil}} + assertExpectedErrorMsgs("nil pointer element", PointerDiveUserValidate(nilElem), []string{ + "Addresses is required", + }) + + emptyElem := &PointerDiveUser{Addresses: []*Address{{}}} + assertExpectedErrorMsgs("pointer element fields", PointerDiveUserValidate(emptyElem), []string{ + "Street is required", + "City is required", + }) + + emptySlice := &PointerDiveUser{} + assertExpectedErrorMsgs("pointer slice empty", PointerDiveUserValidate(emptySlice), []string{ + "Addresses must not be empty", + }) + + pointerOK := &PointerDiveUser{Addresses: []*Address{{Street: "av 123", City: "city 123"}}} + assertExpectedErrorMsgs("pointer dive valid", PointerDiveUserValidate(pointerOK), nil) + + log.Println("dive tests ok") +} diff --git a/tests/endtoend/main.go b/tests/endtoend/main.go index b906cce..1316043 100644 --- a/tests/endtoend/main.go +++ b/tests/endtoend/main.go @@ -35,6 +35,7 @@ func main() { structInPkgTests() nestedStructTests() + diveTests() cmpBetweenInnerFieldsTests() cmpBetweenNestedFieldsTests() boolTests() diff --git a/tests/endtoend/validator__.go b/tests/endtoend/validator__.go index 64f5f68..bf1dc94 100755 --- a/tests/endtoend/validator__.go +++ b/tests/endtoend/validator__.go @@ -271,6 +271,43 @@ func CmpNestedUint8FieldsValidate(obj *CmpNestedUint8Fields) []error { } return errs } +func DiveUserValidate(obj *DiveUser) []error { + var errs []error + if !(len(obj.Addresses) != 0) { + errs = append(errs, types.NewValidationError("Addresses must not be empty")) + } + for _, elem1 := range obj.Addresses { + errs = append(errs, AddressValidate(&elem1)...) + } + for _, elem1 := range obj.Labels { + if !(elem1 != "") { + errs = append(errs, types.NewValidationError("Labels is required")) + } + } + return errs +} +func PointerDiveUserValidate(obj *PointerDiveUser) []error { + var errs []error + if !(len(obj.Addresses) != 0) { + errs = append(errs, types.NewValidationError("Addresses must not be empty")) + } + for _, elem1 := range obj.Addresses { + if !(elem1 != nil) { + errs = append(errs, types.NewValidationError("Addresses is required")) + } + if elem1 != nil { + errs = append(errs, AddressValidate(elem1)...) + } + } + return errs +} +func ShallowDiveUserValidate(obj *ShallowDiveUser) []error { + var errs []error + if !(len(obj.Addresses) != 0) { + errs = append(errs, types.NewValidationError("Addresses must not be empty")) + } + return errs +} func UserValidate(obj *User) []error { var errs []error if !(obj.FirstName != "") {