From 8f361926d61d4e769f5cb1f6ff46bd66355e258e Mon Sep 17 00:00:00 2001 From: Fabian Martin Date: Sat, 27 Aug 2022 15:55:35 +0200 Subject: [PATCH 1/2] fix: Generic Fields does not handle Arrays - Support for *ast.IndexExpr added - tests extended to cover use cases fixes https://github.com/swaggo/swag/issues/1306 --- generics.go | 108 +++++++++++++------- generics_other.go | 20 +++- generics_test.go | 121 ++++++++++++++++------- packages.go | 2 +- parser.go | 6 +- testdata/generics_property/api/api.go | 25 +++++ testdata/generics_property/expected.json | 95 ++++++++++++++++++ testdata/generics_property/main.go | 3 +- 8 files changed, 304 insertions(+), 76 deletions(-) diff --git a/generics.go b/generics.go index 38d74f1a4..2b79ff745 100644 --- a/generics.go +++ b/generics.go @@ -6,10 +6,13 @@ package swag import ( "errors" "fmt" + "github.com/go-openapi/spec" "go/ast" "strings" + "sync" ) +var genericDefinitionsMutex = &sync.RWMutex{} var genericsDefinitions = map[*TypeSpecDef]map[string]*TypeSpecDef{} type genericTypeSpec struct { @@ -55,9 +58,12 @@ func typeSpecFullName(typeSpecDef *TypeSpecDef) string { return fullName } -func (pkgDefs *PackagesDefinitions) parametrizeStruct(original *TypeSpecDef, fullGenericForm string, parseDependency bool) *TypeSpecDef { - if spec, ok := genericsDefinitions[original][fullGenericForm]; ok { - return spec +func (pkgDefs *PackagesDefinitions) parametrizeStruct(file *ast.File, original *TypeSpecDef, fullGenericForm string, parseDependency bool) *TypeSpecDef { + genericDefinitionsMutex.RLock() + tSpec, ok := genericsDefinitions[original][fullGenericForm] + genericDefinitionsMutex.RUnlock() + if ok { + return tSpec } pkgName := strings.Split(fullGenericForm, ".")[0] @@ -81,7 +87,10 @@ func (pkgDefs *PackagesDefinitions) parametrizeStruct(original *TypeSpecDef, ful arrayDepth++ } - tdef := pkgDefs.FindTypeSpec(genericParam, original.File, parseDependency) + tdef := pkgDefs.FindTypeSpec(genericParam, file, parseDependency) + if tdef != nil && !strings.Contains(genericParam, ".") { + genericParam = fullTypeName(file.Name.Name, genericParam) + } genericParamTypeDefs[original.TypeSpec.TypeParams.List[i].Names[0].Name] = &genericTypeSpec{ ArrayDepth: arrayDepth, TypeSpec: tdef, @@ -156,6 +165,8 @@ func (pkgDefs *PackagesDefinitions) parametrizeStruct(original *TypeSpecDef, ful newStructTypeDef.Fields.List = append(newStructTypeDef.Fields.List, newField) } + genericDefinitionsMutex.Lock() + defer genericDefinitionsMutex.Unlock() parametrizedTypeSpec.TypeSpec.Type = newStructTypeDef if genericsDefinitions[original] == nil { genericsDefinitions[original] = map[string]*TypeSpecDef{} @@ -225,27 +236,32 @@ func resolveType(expr ast.Expr, field *ast.Field, genericParamTypeDefs map[strin return field.Type } +func getExtendedGenericFieldType(file *ast.File, field ast.Expr) (string, error) { + switch fieldType := field.(type) { + case *ast.ArrayType: + fieldName, err := getExtendedGenericFieldType(file, fieldType.Elt) + return "[]" + fieldName, err + case *ast.StarExpr: + return getExtendedGenericFieldType(file, fieldType.X) + default: + return getFieldType(file, field) + } +} + func getGenericFieldType(file *ast.File, field ast.Expr) (string, error) { + var fullName string + var baseName string + var err error switch fieldType := field.(type) { case *ast.IndexListExpr: - fullName, err := getGenericTypeName(file, fieldType.X) + baseName, err = getGenericTypeName(file, fieldType.X) if err != nil { return "", err } - fullName += "[" + fullName = baseName + "[" for _, index := range fieldType.Indices { - var fieldName string - var err error - - switch item := index.(type) { - case *ast.ArrayType: - fieldName, err = getFieldType(file, item.Elt) - fieldName = "[]" + fieldName - default: - fieldName, err = getFieldType(file, index) - } - + fieldName, err := getExtendedGenericFieldType(file, index) if err != nil { return "", err } @@ -253,50 +269,76 @@ func getGenericFieldType(file *ast.File, field ast.Expr) (string, error) { fullName += fieldName + "," } - return strings.TrimRight(fullName, ",") + "]", nil + fullName = strings.TrimRight(fullName, ",") + "]" case *ast.IndexExpr: - x, err := getFieldType(file, fieldType.X) + baseName, err = getGenericTypeName(file, fieldType.X) if err != nil { return "", err } - i, err := getFieldType(file, fieldType.Index) + indexName, err := getExtendedGenericFieldType(file, fieldType.Index) if err != nil { return "", err } - packageName := "" - if !strings.Contains(x, ".") { - if file.Name == nil { - return "", errors.New("file name is nil") - } - packageName, _ = getFieldType(file, file.Name) - } + fullName = fmt.Sprintf("%s[%s]", baseName, indexName) + } - return strings.TrimLeft(fmt.Sprintf("%s.%s[%s]", packageName, x, i), "."), nil + if fullName == "" { + return "", fmt.Errorf("unknown field type %#v", field) } - return "", fmt.Errorf("unknown field type %#v", field) + var packageName string + if !strings.Contains(baseName, ".") { + if file.Name == nil { + return "", errors.New("file name is nil") + } + packageName, _ = getFieldType(file, file.Name) + } + + return strings.TrimLeft(fmt.Sprintf("%s.%s", packageName, fullName), "."), nil } func getGenericTypeName(file *ast.File, field ast.Expr) (string, error) { switch indexType := field.(type) { case *ast.Ident: - spec := &TypeSpecDef{ + if indexType.Obj == nil { + return getFieldType(file, field) + } + + tSpec := &TypeSpecDef{ File: file, TypeSpec: indexType.Obj.Decl.(*ast.TypeSpec), PkgPath: file.Name.Name, } - return spec.FullName(), nil + return tSpec.FullName(), nil case *ast.ArrayType: - spec := &TypeSpecDef{ + tSpec := &TypeSpecDef{ File: file, TypeSpec: indexType.Elt.(*ast.Ident).Obj.Decl.(*ast.TypeSpec), PkgPath: file.Name.Name, } - return spec.FullName(), nil + return tSpec.FullName(), nil case *ast.SelectorExpr: return fmt.Sprintf("%s.%s", indexType.X.(*ast.Ident).Name, indexType.Sel.Name), nil } return "", fmt.Errorf("unknown type %#v", field) } + +func (parser *Parser) parseGenericTypeExpr(file *ast.File, typeExpr ast.Expr) (*spec.Schema, error) { + switch expr := typeExpr.(type) { + case *ast.IndexExpr: + name, err := getExtendedGenericFieldType(file, expr) + if err == nil { + if schema, err := parser.getTypeSchema(name, file, false); err == nil { + return spec.MapProperty(schema), nil + } + } else { + parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead. (%s)\n", typeExpr, err) + } + default: + parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) + } + + return PrimitiveSchema(OBJECT), nil +} diff --git a/generics_other.go b/generics_other.go index 9d6aa142b..496815bd8 100644 --- a/generics_other.go +++ b/generics_other.go @@ -5,6 +5,7 @@ package swag import ( "fmt" + "github.com/go-openapi/spec" "go/ast" ) @@ -12,10 +13,27 @@ func typeSpecFullName(typeSpecDef *TypeSpecDef) string { return typeSpecDef.FullName() } -func (pkgDefs *PackagesDefinitions) parametrizeStruct(original *TypeSpecDef, fullGenericForm string, parseDependency bool) *TypeSpecDef { +func (pkgDefs *PackagesDefinitions) parametrizeStruct(file *ast.File, original *TypeSpecDef, fullGenericForm string, parseDependency bool) *TypeSpecDef { return original } func getGenericFieldType(file *ast.File, field ast.Expr) (string, error) { return "", fmt.Errorf("unknown field type %#v", field) } + +func (parser *Parser) parseGenericTypeExpr(file *ast.File, typeExpr ast.Expr) (*spec.Schema, error) { + switch typeExpr.(type) { + case *ast.InterfaceType: + case *ast.StructType: + case *ast.Ident: + case *ast.StarExpr: + case *ast.SelectorExpr: + case *ast.ArrayType: + case *ast.MapType: + case *ast.FuncType: + default: + parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) + } + + return PrimitiveSchema(OBJECT), nil +} diff --git a/generics_test.go b/generics_test.go index e5604188c..f194e1c5a 100644 --- a/generics_test.go +++ b/generics_test.go @@ -98,55 +98,67 @@ func TestParametrizeStruct(t *testing.T) { packages: make(map[string]*PackageDefinitions), } // valid - typeSpec := pd.parametrizeStruct(&TypeSpecDef{ - TypeSpec: &ast.TypeSpec{ - Name: &ast.Ident{Name: "Field"}, - TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, - Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, - }}, "test.Field[string, []string]", false) + typeSpec := pd.parametrizeStruct( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string, []string]", false) assert.Equal(t, "$test.Field-string-array_string", typeSpec.Name()) // definition contains one type params, but two type params are provided - typeSpec = pd.parametrizeStruct(&TypeSpecDef{ - TypeSpec: &ast.TypeSpec{ - Name: &ast.Ident{Name: "Field"}, - TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}}}, - Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, - }}, "test.Field[string, string]", false) + typeSpec = pd.parametrizeStruct( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string, string]", false) assert.Nil(t, typeSpec) // definition contains two type params, but only one is used - typeSpec = pd.parametrizeStruct(&TypeSpecDef{ - TypeSpec: &ast.TypeSpec{ - Name: &ast.Ident{Name: "Field"}, - TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, - Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, - }}, "test.Field[string]", false) + typeSpec = pd.parametrizeStruct( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string]", false) assert.Nil(t, typeSpec) // name is not a valid type name - typeSpec = pd.parametrizeStruct(&TypeSpecDef{ - TypeSpec: &ast.TypeSpec{ - Name: &ast.Ident{Name: "Field"}, - TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, - Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, - }}, "test.Field[string", false) + typeSpec = pd.parametrizeStruct( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string", false) assert.Nil(t, typeSpec) - typeSpec = pd.parametrizeStruct(&TypeSpecDef{ - TypeSpec: &ast.TypeSpec{ - Name: &ast.Ident{Name: "Field"}, - TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, - Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, - }}, "test.Field[string, [string]", false) + typeSpec = pd.parametrizeStruct( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string, [string]", false) assert.Nil(t, typeSpec) - typeSpec = pd.parametrizeStruct(&TypeSpecDef{ - TypeSpec: &ast.TypeSpec{ - Name: &ast.Ident{Name: "Field"}, - TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, - Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, - }}, "test.Field[string, ]string]", false) + typeSpec = pd.parametrizeStruct( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string, ]string]", false) assert.Nil(t, typeSpec) } @@ -294,3 +306,40 @@ func TestGetGenericTypeName(t *testing.T) { ) assert.Error(t, err) } + +func TestParseGenericTypeExpr(t *testing.T) { + t.Parallel() + + parser := New() + parser.packages.uniqueDefinitions["field.Name[string]"] = &TypeSpecDef{ + File: &ast.File{Name: &ast.Ident{Name: "test"}}, + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }, + } + spec, err := parser.parseTypeExpr( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexExpr{X: &ast.SelectorExpr{X: &ast.Ident{Name: "field"}, Sel: &ast.Ident{Name: "Name"}}, Index: &ast.Ident{Name: "string"}}, + false, + ) + assert.NotNil(t, spec) + assert.NoError(t, err) + + spec, err = parser.parseTypeExpr( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexExpr{X: &ast.BadExpr{}, Index: &ast.Ident{Name: "string"}}, + false, + ) + assert.NotNil(t, spec) + assert.Equal(t, "object", spec.SchemaProps.Type[0]) + + spec, err = parser.parseTypeExpr( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.BadExpr{}, + false, + ) + assert.NotNil(t, spec) + assert.Equal(t, "object", spec.SchemaProps.Type[0]) +} diff --git a/packages.go b/packages.go index e7cf7a38b..25afe9753 100644 --- a/packages.go +++ b/packages.go @@ -398,7 +398,7 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File } if strings.Contains(tName, joinedParts) { - if parametrized := pkgDefs.parametrizeStruct(tSpec, typeName, parseDependency); parametrized != nil { + if parametrized := pkgDefs.parametrizeStruct(file, tSpec, typeName, parseDependency); parametrized != nil { return parametrized } } diff --git a/parser.go b/parser.go index f7250b88e..a47187989 100644 --- a/parser.go +++ b/parser.go @@ -1186,12 +1186,10 @@ func (parser *Parser) parseTypeExpr(file *ast.File, typeExpr ast.Expr, ref bool) case *ast.FuncType: return nil, ErrFuncTypeField - // ... - default: - parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) + // ... } - return PrimitiveSchema(OBJECT), nil + return parser.parseGenericTypeExpr(file, typeExpr) } func (parser *Parser) parseStruct(file *ast.File, fields *ast.FieldList) (*spec.Schema, error) { diff --git a/testdata/generics_property/api/api.go b/testdata/generics_property/api/api.go index 2e6fc7caf..8f45eee8c 100644 --- a/testdata/generics_property/api/api.go +++ b/testdata/generics_property/api/api.go @@ -1,12 +1,27 @@ package api import ( + "github.com/swaggo/swag/testdata/generics_property/types" "github.com/swaggo/swag/testdata/generics_property/web" "net/http" ) type NestedResponse struct { web.GenericResponse[[]string, *uint8] + Post types.Field[[]types.Post] +} + +type CreateMovie struct { + Name string + MainActor types.Field[Person] + SupportingCast types.Field[[]Person] + Directors types.Field[*[]Person] + CameraPeople types.Field[[]*Person] + Producer types.Field[*Person] +} + +type Person struct { + Name string } // @Summary List Posts @@ -21,3 +36,13 @@ type NestedResponse struct { // @Router /posts [get] func GetPosts(w http.ResponseWriter, r *http.Request) { } + +// @Summary Create movie +// @Description Create a new movie production +// @Accept json +// @Produce json +// @Param data body CreateMovie true "Movie Create-Payload" +// @Success 201 {object} CreateMovie "ok" +// @Router /movie [post] +func CreateMovieApi(w http.ResponseWriter, r *http.Request) { +} diff --git a/testdata/generics_property/expected.json b/testdata/generics_property/expected.json index 24b6f94c4..65b394e5b 100644 --- a/testdata/generics_property/expected.json +++ b/testdata/generics_property/expected.json @@ -9,6 +9,37 @@ "host": "localhost:4000", "basePath": "/api", "paths": { + "/movie": { + "post": { + "description": "Create a new movie production", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "parameters": [ + { + "description": "Movie Create-Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.CreateMovie" + } + } + ], + "responses": { + "201": { + "description": "ok", + "schema": { + "$ref": "#/definitions/api.CreateMovie" + } + } + } + } + }, "/posts": { "get": { "description": "Get All of the Posts", @@ -71,6 +102,29 @@ } }, "definitions": { + "api.CreateMovie": { + "type": "object", + "properties": { + "cameraPeople": { + "$ref": "#/definitions/types.Field-array_api_Person" + }, + "directors": { + "$ref": "#/definitions/types.Field-array_api_Person" + }, + "mainActor": { + "$ref": "#/definitions/types.Field-api_Person" + }, + "name": { + "type": "string" + }, + "producer": { + "$ref": "#/definitions/types.Field-api_Person" + }, + "supportingCast": { + "$ref": "#/definitions/types.Field-array_api_Person" + } + } + }, "api.NestedResponse": { "type": "object", "properties": { @@ -82,6 +136,47 @@ }, "items2": { "type": "integer" + }, + "post": { + "$ref": "#/definitions/types.Field-array_types_Post" + } + } + }, + "api.Person": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "types.Field-api_Person": { + "type": "object", + "properties": { + "value": { + "$ref": "#/definitions/api.Person" + } + } + }, + "types.Field-array_api_Person": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Person" + } + } + } + }, + "types.Field-array_types_Post": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } } } }, diff --git a/testdata/generics_property/main.go b/testdata/generics_property/main.go index 8d79868fc..41b58c6e3 100644 --- a/testdata/generics_property/main.go +++ b/testdata/generics_property/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/generics_arrays/api" + "github.com/swaggo/swag/testdata/generics_property/api" ) // @title Swagger Example API @@ -13,5 +13,6 @@ import ( // @basePath /api func main() { http.HandleFunc("/posts/", api.GetPosts) + http.HandleFunc("/movie/", api.CreateMovieApi) http.ListenAndServe(":8080", nil) } From 3d8255d69a7470eec1a2a6e79de43e4b029d2169 Mon Sep 17 00:00:00 2001 From: Fabian Martin Date: Sat, 27 Aug 2022 21:53:21 +0200 Subject: [PATCH 2/2] test: Extend tests to improve code coverage --- generics.go | 13 ++++++-- generics_other.go | 1 + generics_other_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++ generics_test.go | 38 ++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 generics_other_test.go diff --git a/generics.go b/generics.go index 2b79ff745..df823b264 100644 --- a/generics.go +++ b/generics.go @@ -327,15 +327,24 @@ func getGenericTypeName(file *ast.File, field ast.Expr) (string, error) { func (parser *Parser) parseGenericTypeExpr(file *ast.File, typeExpr ast.Expr) (*spec.Schema, error) { switch expr := typeExpr.(type) { + // suppress debug messages for these types + case *ast.InterfaceType: + case *ast.StructType: + case *ast.Ident: + case *ast.StarExpr: + case *ast.SelectorExpr: + case *ast.ArrayType: + case *ast.MapType: + case *ast.FuncType: case *ast.IndexExpr: name, err := getExtendedGenericFieldType(file, expr) if err == nil { if schema, err := parser.getTypeSchema(name, file, false); err == nil { return spec.MapProperty(schema), nil } - } else { - parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead. (%s)\n", typeExpr, err) } + + parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead. (%s)\n", typeExpr, err) default: parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) } diff --git a/generics_other.go b/generics_other.go index 496815bd8..ddfca85b9 100644 --- a/generics_other.go +++ b/generics_other.go @@ -23,6 +23,7 @@ func getGenericFieldType(file *ast.File, field ast.Expr) (string, error) { func (parser *Parser) parseGenericTypeExpr(file *ast.File, typeExpr ast.Expr) (*spec.Schema, error) { switch typeExpr.(type) { + // suppress debug messages for these types case *ast.InterfaceType: case *ast.StructType: case *ast.Ident: diff --git a/generics_other_test.go b/generics_other_test.go new file mode 100644 index 000000000..fafe2e2f2 --- /dev/null +++ b/generics_other_test.go @@ -0,0 +1,67 @@ +//go:build !go1.18 +// +build !go1.18 + +package swag + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "go/ast" + "testing" +) + +type testLogger struct { + Messages []string +} + +func (t *testLogger) Printf(format string, v ...interface{}) { + t.Messages = append(t.Messages, fmt.Sprintf(format, v...)) +} + +func TestParametrizeStruct(t *testing.T) { + t.Parallel() + + pd := PackagesDefinitions{ + packages: make(map[string]*PackageDefinitions), + } + + tSpec := &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }, + } + + tr := pd.parametrizeStruct(&ast.File{}, tSpec, "", false) + assert.Equal(t, tr, tSpec) + + tr = pd.parametrizeStruct(&ast.File{}, tSpec, "", true) + assert.Equal(t, tr, tSpec) +} + +func TestParseGenericTypeExpr(t *testing.T) { + t.Parallel() + + parser := New() + logger := &testLogger{} + SetDebugger(logger)(parser) + + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.InterfaceType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.StructType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.Ident{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.StarExpr{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.SelectorExpr{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.ArrayType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.MapType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.FuncType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.BadExpr{}) + assert.NotEmpty(t, logger.Messages) +} diff --git a/generics_test.go b/generics_test.go index f194e1c5a..b8fe45856 100644 --- a/generics_test.go +++ b/generics_test.go @@ -5,6 +5,7 @@ package swag import ( "encoding/json" + "fmt" "go/ast" "io/ioutil" "path/filepath" @@ -13,6 +14,14 @@ import ( "github.com/stretchr/testify/assert" ) +type testLogger struct { + Messages []string +} + +func (t *testLogger) Printf(format string, v ...interface{}) { + t.Messages = append(t.Messages, fmt.Sprintf(format, v...)) +} + func TestParseGenericsBasic(t *testing.T) { t.Parallel() @@ -311,6 +320,29 @@ func TestParseGenericTypeExpr(t *testing.T) { t.Parallel() parser := New() + logger := &testLogger{} + SetDebugger(logger)(parser) + + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.InterfaceType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.StructType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.Ident{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.StarExpr{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.SelectorExpr{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.ArrayType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.MapType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.FuncType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.BadExpr{}) + assert.NotEmpty(t, logger.Messages) + assert.Len(t, logger.Messages, 1) + parser.packages.uniqueDefinitions["field.Name[string]"] = &TypeSpecDef{ File: &ast.File{Name: &ast.Ident{Name: "test"}}, TypeSpec: &ast.TypeSpec{ @@ -327,6 +359,7 @@ func TestParseGenericTypeExpr(t *testing.T) { assert.NotNil(t, spec) assert.NoError(t, err) + logger.Messages = []string{} spec, err = parser.parseTypeExpr( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.IndexExpr{X: &ast.BadExpr{}, Index: &ast.Ident{Name: "string"}}, @@ -334,7 +367,10 @@ func TestParseGenericTypeExpr(t *testing.T) { ) assert.NotNil(t, spec) assert.Equal(t, "object", spec.SchemaProps.Type[0]) + assert.NotEmpty(t, logger.Messages) + assert.Len(t, logger.Messages, 1) + logger.Messages = []string{} spec, err = parser.parseTypeExpr( &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.BadExpr{}, @@ -342,4 +378,6 @@ func TestParseGenericTypeExpr(t *testing.T) { ) assert.NotNil(t, spec) assert.Equal(t, "object", spec.SchemaProps.Type[0]) + assert.NotEmpty(t, logger.Messages) + assert.Len(t, logger.Messages, 1) }