diff --git a/generics.go b/generics.go index bb6a09961..07344bbaf 100644 --- a/generics.go +++ b/generics.go @@ -14,9 +14,8 @@ import ( ) type genericTypeSpec struct { - ArrayDepth int - TypeSpec *TypeSpecDef - Name string + TypeSpec *TypeSpecDef + Name string } type formalParamType struct { @@ -31,6 +30,87 @@ func (t *genericTypeSpec) TypeName() string { return t.Name } +func normalizeGenericTypeName(name string) string { + return strings.Replace(name, ".", "_", -1) +} + +func (pkgDefs *PackagesDefinitions) getTypeFromGenericParam(genericParam string, file *ast.File) (typeSpecDef *TypeSpecDef) { + if strings.HasPrefix(genericParam, "[]") { + typeSpecDef = pkgDefs.getTypeFromGenericParam(genericParam[2:], file) + if typeSpecDef == nil { + return nil + } + var expr ast.Expr + switch typeSpecDef.TypeSpec.Type.(type) { + case *ast.ArrayType, *ast.MapType: + expr = typeSpecDef.TypeSpec.Type + default: + name := typeSpecDef.TypeName() + expr = ast.NewIdent(name) + if _, ok := pkgDefs.uniqueDefinitions[name]; !ok { + pkgDefs.uniqueDefinitions[name] = typeSpecDef + } + } + return &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: ast.NewIdent(string(IgnoreNameOverridePrefix) + "array_" + typeSpecDef.TypeName()), + Type: &ast.ArrayType{ + Elt: expr, + }, + }, + Enums: typeSpecDef.Enums, + PkgPath: typeSpecDef.PkgPath, + ParentSpec: typeSpecDef.ParentSpec, + NotUnique: false, + } + } + + if strings.HasPrefix(genericParam, "map[") { + parts := strings.SplitN(genericParam[4:], "]", 2) + if len(parts) != 2 { + return nil + } + typeSpecDef = pkgDefs.getTypeFromGenericParam(parts[1], file) + if typeSpecDef == nil { + return nil + } + var expr ast.Expr + switch typeSpecDef.TypeSpec.Type.(type) { + case *ast.ArrayType, *ast.MapType: + expr = typeSpecDef.TypeSpec.Type + default: + name := typeSpecDef.TypeName() + expr = ast.NewIdent(name) + if _, ok := pkgDefs.uniqueDefinitions[name]; !ok { + pkgDefs.uniqueDefinitions[name] = typeSpecDef + } + } + return &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: ast.NewIdent(string(IgnoreNameOverridePrefix) + "map_" + parts[0] + "_" + typeSpecDef.TypeName()), + Type: &ast.MapType{ + Key: ast.NewIdent(parts[0]), //assume key is string or integer + Value: expr, + }, + }, + Enums: typeSpecDef.Enums, + PkgPath: typeSpecDef.PkgPath, + ParentSpec: typeSpecDef.ParentSpec, + NotUnique: false, + } + + } + if IsGolangPrimitiveType(genericParam) { + return &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: ast.NewIdent(genericParam), + Type: ast.NewIdent(genericParam), + }, + } + } + return pkgDefs.FindTypeSpec(genericParam, file) +} + func (pkgDefs *PackagesDefinitions) parametrizeGenericType(file *ast.File, original *TypeSpecDef, fullGenericForm string) *TypeSpecDef { if original == nil || original.TypeSpec.TypeParams == nil || len(original.TypeSpec.TypeParams.List) == 0 { return original @@ -58,27 +138,19 @@ func (pkgDefs *PackagesDefinitions) parametrizeGenericType(file *ast.File, origi genericParamTypeDefs := map[string]*genericTypeSpec{} for i, genericParam := range genericParams { - arrayDepth := 0 - for { - if len(genericParam) <= 2 || genericParam[:2] != "[]" { - break + var typeDef *TypeSpecDef + if !IsGolangPrimitiveType(genericParam) { + typeDef = pkgDefs.getTypeFromGenericParam(genericParam, file) + if typeDef != nil { + genericParam = typeDef.TypeName() + if _, ok := pkgDefs.uniqueDefinitions[genericParam]; !ok { + pkgDefs.uniqueDefinitions[genericParam] = typeDef + } } - genericParam = genericParam[2:] - arrayDepth++ } - - typeDef := pkgDefs.FindTypeSpec(genericParam, file) - if typeDef != nil { - genericParam = typeDef.TypeName() - if _, ok := pkgDefs.uniqueDefinitions[genericParam]; !ok { - pkgDefs.uniqueDefinitions[genericParam] = typeDef - } - } - genericParamTypeDefs[formals[i].Name] = &genericTypeSpec{ - ArrayDepth: arrayDepth, - TypeSpec: typeDef, - Name: genericParam, + TypeSpec: typeDef, + Name: genericParam, } } @@ -86,17 +158,11 @@ func (pkgDefs *PackagesDefinitions) parametrizeGenericType(file *ast.File, origi var nameParts []string for _, def := range formals { if specDef, ok := genericParamTypeDefs[def.Name]; ok { - var prefix = "" - if specDef.ArrayDepth == 1 { - prefix = "array_" - } else if specDef.ArrayDepth > 1 { - prefix = fmt.Sprintf("array%d_", specDef.ArrayDepth) - } - nameParts = append(nameParts, prefix+specDef.TypeName()) + nameParts = append(nameParts, specDef.TypeName()) } } - name += strings.Replace(strings.Join(nameParts, "-"), ".", "_", -1) + name += normalizeGenericTypeName(strings.Join(nameParts, "-")) if typeSpec, ok := pkgDefs.uniqueDefinitions[name]; ok { return typeSpec @@ -180,11 +246,7 @@ func (pkgDefs *PackagesDefinitions) resolveGenericType(file *ast.File, expr ast. switch astExpr := expr.(type) { case *ast.Ident: if genTypeSpec, ok := genericParamTypeDefs[astExpr.Name]; ok { - retType := pkgDefs.getParametrizedType(genTypeSpec) - for i := 0; i < genTypeSpec.ArrayDepth; i++ { - retType = &ast.ArrayType{Elt: retType} - } - return retType + return pkgDefs.getParametrizedType(genTypeSpec) } case *ast.ArrayType: return &ast.ArrayType{ diff --git a/generics_test.go b/generics_test.go index 33a92d9af..033bcf546 100644 --- a/generics_test.go +++ b/generics_test.go @@ -7,7 +7,6 @@ import ( "encoding/json" "fmt" "go/ast" - "io/fs" "os" "path/filepath" "testing" @@ -55,6 +54,7 @@ func TestParseGenericsArrays(t *testing.T) { err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, err := json.MarshalIndent(p.swagger, "", " ") + assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } @@ -100,7 +100,6 @@ func TestParseGenericsProperty(t *testing.T) { err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, err := json.MarshalIndent(p.swagger, "", " ") - os.WriteFile(searchDir+"/expected.json", b, fs.ModePerm) assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } diff --git a/parser.go b/parser.go index e52e9b07d..604f8278e 100644 --- a/parser.go +++ b/parser.go @@ -1283,6 +1283,9 @@ func fullTypeName(parts ...string) string { // fillDefinitionDescription additionally fills fields in definition (spec.Schema) // TODO: If .go file contains many types, it may work for a long time func fillDefinitionDescription(definition *spec.Schema, file *ast.File, typeSpecDef *TypeSpecDef) { + if file == nil { + return + } for _, astDeclaration := range file.Decls { generalDeclaration, ok := astDeclaration.(*ast.GenDecl) if !ok || generalDeclaration.Tok != token.TYPE { diff --git a/testdata/generics_basic/api/api.go b/testdata/generics_basic/api/api.go index eecd72f03..a8a98d265 100644 --- a/testdata/generics_basic/api/api.go +++ b/testdata/generics_basic/api/api.go @@ -39,6 +39,7 @@ type Foo = web.GenericResponseMulti[types.Post, types.Post] // @Success 204 {object} Response[string, types.Field[int]] // @Success 205 {object} Response[StringStruct, types.Field[int]] // @Success 206 {object} Response2[string, types.Field[int],string] +// @Success 207 {object} Response[[]map[string]string, map[string][]types.Field[int]] // @Success 222 {object} web.GenericResponseMulti[types.Post, types.Post] // @Failure 400 {object} web.APIError "We need ID!!" // @Failure 404 {object} web.APIError "Can not find ID" diff --git a/testdata/generics_basic/expected.json b/testdata/generics_basic/expected.json index 4aeeec88c..58689cf74 100644 --- a/testdata/generics_basic/expected.json +++ b/testdata/generics_basic/expected.json @@ -81,7 +81,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/web.GenericBodyMulti-array_types_Post-array2_types_Post" + "$ref": "#/definitions/web.GenericBodyMulti-array_types_Post-array_array_types_Post" } } ], @@ -101,7 +101,7 @@ "222": { "description": "", "schema": { - "$ref": "#/definitions/web.GenericResponseMulti-array_types_Post-array2_types_Post" + "$ref": "#/definitions/web.GenericResponseMulti-array_types_Post-array_array_types_Post" } } } @@ -171,6 +171,12 @@ "$ref": "#/definitions/api.Response2-string-types_Field-int-string" } }, + "207": { + "description": "Multi-Status", + "schema": { + "$ref": "#/definitions/api.Response-array_map_string_string-map_string_array_types_Field-int" + } + }, "222": { "description": "", "schema": { @@ -222,6 +228,26 @@ } } }, + "api.Response-array_map_string_string-map_string_array_types_Field-int": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "meta": { + "$ref": "#/definitions/map_string_array_types.Field-int" + }, + "status": { + "type": "string" + } + } + }, "api.Response-string-types_Field-int": { "type": "object", "properties": { @@ -258,6 +284,15 @@ } } }, + "map_string_array_types.Field-int": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Field-int" + } + } + }, "types.Field-int": { "type": "object", "properties": { @@ -408,7 +443,7 @@ } } }, - "web.GenericBodyMulti-array_types_Post-array2_types_Post": { + "web.GenericBodyMulti-array_types_Post-array_array_types_Post": { "type": "object", "properties": { "data": { @@ -511,7 +546,7 @@ } } }, - "web.GenericResponseMulti-array_types_Post-array2_types_Post": { + "web.GenericResponseMulti-array_types_Post-array_array_types_Post": { "type": "object", "properties": { "data": { diff --git a/testdata/generics_names/expected.json b/testdata/generics_names/expected.json index 741b2455d..5da1de542 100644 --- a/testdata/generics_names/expected.json +++ b/testdata/generics_names/expected.json @@ -63,7 +63,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/MultiBody-array_Post-array2_Post" + "$ref": "#/definitions/MultiBody-array_Post-array_array_Post" } } ], @@ -77,7 +77,7 @@ "222": { "description": "", "schema": { - "$ref": "#/definitions/MultiResponse-array_Post-array2_Post" + "$ref": "#/definitions/MultiResponse-array_Post-array_array_Post" } } } @@ -173,7 +173,7 @@ } } }, - "MultiBody-array_Post-array2_Post": { + "MultiBody-array_Post-array_array_Post": { "type": "object", "properties": { "data": { @@ -207,7 +207,7 @@ } } }, - "MultiResponse-array_Post-array2_Post": { + "MultiResponse-array_Post-array_array_Post": { "type": "object", "properties": { "data": { diff --git a/testdata/generics_nested/expected.json b/testdata/generics_nested/expected.json index 51b53a1c5..2dfe1e0ee 100644 --- a/testdata/generics_nested/expected.json +++ b/testdata/generics_nested/expected.json @@ -119,7 +119,7 @@ "205": { "description": "Reset Content", "schema": { - "$ref": "#/definitions/web.GenericNestedResponseMulti-types_Post-web_GenericInnerMultiType-types_Post-array_web_GenericInnerType-array2_types_Post" + "$ref": "#/definitions/web.GenericNestedResponseMulti-types_Post-web_GenericInnerMultiType-types_Post-array_web_GenericInnerType-array_array_types_Post" } }, "222": { @@ -203,7 +203,7 @@ } } }, - "web.GenericInnerMultiType-types_Post-array_web_GenericInnerType-array2_types_Post": { + "web.GenericInnerMultiType-types_Post-array_web_GenericInnerType-array_array_types_Post": { "type": "object", "properties": { "itemOne": { @@ -220,7 +220,7 @@ "items": { "type": "array", "items": { - "$ref": "#/definitions/web.GenericInnerType-array2_types_Post" + "$ref": "#/definitions/web.GenericInnerType-array_array_types_Post" } } } @@ -266,7 +266,7 @@ } } }, - "web.GenericInnerType-array2_types_Post": { + "web.GenericInnerType-array_array_types_Post": { "type": "object", "properties": { "items": { @@ -478,7 +478,7 @@ } } }, - "web.GenericNestedResponseMulti-types_Post-web_GenericInnerMultiType-types_Post-array_web_GenericInnerType-array2_types_Post": { + "web.GenericNestedResponseMulti-types_Post-web_GenericInnerMultiType-types_Post-array_web_GenericInnerType-array_array_types_Post": { "type": "object", "properties": { "itemOne": { @@ -493,7 +493,7 @@ "description": "ItemsTwo is the second thing", "type": "array", "items": { - "$ref": "#/definitions/web.GenericInnerMultiType-types_Post-array_web_GenericInnerType-array2_types_Post" + "$ref": "#/definitions/web.GenericInnerMultiType-types_Post-array_web_GenericInnerType-array_array_types_Post" } }, "status": { diff --git a/testdata/generics_property/expected.json b/testdata/generics_property/expected.json index 490909ee0..fe67258a1 100644 --- a/testdata/generics_property/expected.json +++ b/testdata/generics_property/expected.json @@ -213,7 +213,7 @@ } }, "value4": { - "$ref": "#/definitions/types.SubField1-api_Person-string" + "$ref": "#/definitions/types.SubField1-array_api_Person-string" } } }, @@ -242,7 +242,7 @@ } }, "value4": { - "$ref": "#/definitions/types.SubField1-types_Post-string" + "$ref": "#/definitions/types.SubField1-array_types_Post-string" } } }, @@ -348,44 +348,61 @@ } } }, - "types.SubField1-string-string": { + "types.SubField1-array_api_Person-string": { "type": "object", "properties": { "subValue1": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Person" + } + }, + "subValue2": { "type": "string" + } + } + }, + "types.SubField1-array_types_Post-string": { + "type": "object", + "properties": { + "subValue1": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } }, "subValue2": { "type": "string" } } }, - "types.SubField1-types_Field-api_Person-string": { + "types.SubField1-string-string": { "type": "object", "properties": { "subValue1": { - "$ref": "#/definitions/types.Field-api_Person" + "type": "string" }, "subValue2": { "type": "string" } } }, - "types.SubField1-types_Field-string-string": { + "types.SubField1-types_Field-api_Person-string": { "type": "object", "properties": { "subValue1": { - "$ref": "#/definitions/types.Field-string" + "$ref": "#/definitions/types.Field-api_Person" }, "subValue2": { "type": "string" } } }, - "types.SubField1-types_Post-string": { + "types.SubField1-types_Field-string-string": { "type": "object", "properties": { "subValue1": { - "$ref": "#/definitions/types.Post" + "$ref": "#/definitions/types.Field-string" }, "subValue2": { "type": "string" diff --git a/types.go b/types.go index ebb373d7f..0076a6b40 100644 --- a/types.go +++ b/types.go @@ -35,7 +35,7 @@ type TypeSpecDef struct { // Name the name of the typeSpec. func (t *TypeSpecDef) Name() string { - if t.TypeSpec != nil { + if t.TypeSpec != nil && t.TypeSpec.Name != nil { return t.TypeSpec.Name.Name } @@ -71,7 +71,7 @@ func (t *TypeSpecDef) TypeName() string { return r }, t.PkgPath) names = append(names, pkgPath) - } else { + } else if t.File != nil { names = append(names, t.File.Name.Name) } if parentFun, ok := (t.ParentSpec).(*ast.FuncDecl); ok && parentFun != nil {