diff --git a/packages.go b/packages.go index 3750c858f..070d48d16 100644 --- a/packages.go +++ b/packages.go @@ -107,6 +107,7 @@ func (pkgDefs *PackagesDefinitions) ParseTypes() (map[*TypeSpecDef]*Schema, erro parsedSchemas := make(map[*TypeSpecDef]*Schema) for astFile, info := range pkgDefs.files { pkgDefs.parseTypesFromFile(astFile, info.PackagePath, parsedSchemas) + pkgDefs.parseFunctionScopedTypesFromFile(astFile, info.PackagePath, parsedSchemas) } return parsedSchemas, nil } @@ -161,6 +162,64 @@ func (pkgDefs *PackagesDefinitions) parseTypesFromFile(astFile *ast.File, packag } } +func (pkgDefs *PackagesDefinitions) parseFunctionScopedTypesFromFile(astFile *ast.File, packagePath string, parsedSchemas map[*TypeSpecDef]*Schema) { + for _, astDeclaration := range astFile.Decls { + if funcDeclaration, ok := astDeclaration.(*ast.FuncDecl); ok { + for _, stmt := range funcDeclaration.Body.List { + if declStmt, ok := (stmt).(*ast.DeclStmt); ok { + if genDecl, ok := (declStmt.Decl).(*ast.GenDecl); ok && genDecl.Tok == token.TYPE { + for _, astSpec := range genDecl.Specs { + if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { + typeSpecDef := &TypeSpecDef{ + PkgPath: packagePath, + File: astFile, + TypeSpec: typeSpec, + ParentSpec: astDeclaration, + } + + if idt, ok := typeSpec.Type.(*ast.Ident); ok && IsGolangPrimitiveType(idt.Name) && parsedSchemas != nil { + parsedSchemas[typeSpecDef] = &Schema{ + PkgPath: typeSpecDef.PkgPath, + Name: astFile.Name.Name, + Schema: PrimitiveSchema(TransToValidSchemeType(idt.Name)), + } + } + + if pkgDefs.uniqueDefinitions == nil { + pkgDefs.uniqueDefinitions = make(map[string]*TypeSpecDef) + } + + fullName := typeSpecFullName(typeSpecDef) + + anotherTypeDef, ok := pkgDefs.uniqueDefinitions[fullName] + if ok { + if typeSpecDef.PkgPath == anotherTypeDef.PkgPath { + continue + } else { + delete(pkgDefs.uniqueDefinitions, fullName) + } + } else { + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } + + if pkgDefs.packages[typeSpecDef.PkgPath] == nil { + pkgDefs.packages[typeSpecDef.PkgPath] = &PackageDefinitions{ + Name: astFile.Name.Name, + TypeDefinitions: map[string]*TypeSpecDef{fullName: typeSpecDef}, + } + } else if _, ok = pkgDefs.packages[typeSpecDef.PkgPath].TypeDefinitions[fullName]; !ok { + pkgDefs.packages[typeSpecDef.PkgPath].TypeDefinitions[fullName] = typeSpecDef + } + } + } + + } + } + } + } + } +} + func (pkgDefs *PackagesDefinitions) findTypeSpec(pkgPath string, typeName string) *TypeSpecDef { if pkgDefs.packages == nil { return nil diff --git a/packages_test.go b/packages_test.go index d74ba4d3a..ad012ee92 100644 --- a/packages_test.go +++ b/packages_test.go @@ -111,6 +111,51 @@ func TestPackagesDefinitions_ParseTypes(t *testing.T) { assert.NoError(t, err) } +func TestPackagesDefinitions_parseFunctionScopedTypesFromFile(t *testing.T) { + mainAST := &ast.File{ + Name: &ast.Ident{Name: "main.go"}, + Decls: []ast.Decl{ + &ast.FuncDecl{ + Name: ast.NewIdent("TestFuncDecl"), + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.DeclStmt{ + Decl: &ast.GenDecl{ + Tok: token.TYPE, + Specs: []ast.Spec{ + &ast.TypeSpec{ + Name: ast.NewIdent("response"), + Type: ast.NewIdent("struct"), + }, + &ast.TypeSpec{ + Name: ast.NewIdent("stringResponse"), + Type: ast.NewIdent("string"), + }, + }, + }, + }, + }, + }, + }, + }, + } + + pd := PackagesDefinitions{ + packages: make(map[string]*PackageDefinitions), + } + + parsedSchema := make(map[*TypeSpecDef]*Schema) + pd.parseFunctionScopedTypesFromFile(mainAST, "main", parsedSchema) + + assert.Len(t, parsedSchema, 1) + + _, ok := pd.uniqueDefinitions["main.go.TestFuncDecl.response"] + assert.True(t, ok) + + _, ok = pd.packages["main"].TypeDefinitions["main.go.TestFuncDecl.response"] + assert.True(t, ok) +} + func TestPackagesDefinitions_FindTypeSpec(t *testing.T) { userDef := TypeSpecDef{ File: &ast.File{ diff --git a/parser.go b/parser.go index ec78bb55b..f7250b88e 100644 --- a/parser.go +++ b/parser.go @@ -1016,7 +1016,12 @@ func (parser *Parser) isInStructStack(typeSpecDef *TypeSpecDef) bool { // with a schema for the given type func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) { typeName := typeSpecDef.FullName() - refTypeName := TypeDocName(typeName, typeSpecDef.TypeSpec) + var refTypeName string + if fn, ok := (typeSpecDef.ParentSpec).(*ast.FuncDecl); ok { + refTypeName = TypeDocNameFuncScoped(typeName, typeSpecDef.TypeSpec, fn.Name.Name) + } else { + refTypeName = TypeDocName(typeName, typeSpecDef.TypeSpec) + } schema, found := parser.parsedSchemas[typeSpecDef] if found { @@ -1073,6 +1078,14 @@ func fullTypeName(pkgName, typeName string) string { return typeName } +func fullTypeNameFunctionScoped(pkgName, fnName, typeName string) string { + if pkgName != "" { + return pkgName + "." + fnName + "." + typeName + } + + return typeName +} + // 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) { diff --git a/parser_test.go b/parser_test.go index d5eb14a2d..b6e66d633 100644 --- a/parser_test.go +++ b/parser_test.go @@ -148,6 +148,28 @@ func TestParser_ParseDefinition(t *testing.T) { } _, err = p.ParseDefinition(definition) assert.Error(t, err) + + // Parsing *ast.FuncType with parent spec + definition = &TypeSpecDef{ + PkgPath: "github.com/swagger/swag/model", + File: &ast.File{ + Name: &ast.Ident{ + Name: "model", + }, + }, + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{ + Name: "Test", + }, + Type: &ast.FuncType{}, + }, + ParentSpec: &ast.FuncDecl{ + Name: ast.NewIdent("TestFuncDecl"), + }, + } + _, err = p.ParseDefinition(definition) + assert.Error(t, err) + assert.Equal(t, "model.TestFuncDecl.Test", definition.FullName()) } func TestParser_ParseGeneralApiInfo(t *testing.T) { @@ -2167,6 +2189,16 @@ func TestParseDuplicatedOtherMethods(t *testing.T) { assert.Errorf(t, err, "duplicated @id declarations successfully found") } +func TestParseDuplicatedFunctionScoped(t *testing.T) { + t.Parallel() + + searchDir := "testdata/duplicated_function_scoped" + p := New() + p.ParseDependency = true + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.Errorf(t, err, "duplicated @id declarations successfully found") +} + func TestParseConflictSchemaName(t *testing.T) { t.Parallel() @@ -3233,6 +3265,130 @@ func Fun() { assert.Equal(t, "#/definitions/Teacher", ref.String()) } +func TestParseFunctionScopedStructDefinition(t *testing.T) { + t.Parallel() + + src := ` +package main + +// @Param request body main.Fun.request true "query params" +// @Success 200 {object} main.Fun.response +// @Router /test [post] +func Fun() { + type request struct { + Name string + } + + type response struct { + Name string + Child string + } +} +` + f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments) + assert.NoError(t, err) + + p := New() + _ = p.packages.CollectAstFile("api", "api/api.go", f) + _, err = p.packages.ParseTypes() + assert.NoError(t, err) + + err = p.ParseRouterAPIInfo("", f) + assert.NoError(t, err) + + _, ok := p.swagger.Definitions["main.Fun.response"] + assert.True(t, ok) +} + +func TestParseFunctionScopedStructRequestResponseJSON(t *testing.T) { + t.Parallel() + + src := ` +package main + +// @Param request body main.Fun.request true "query params" +// @Success 200 {object} main.Fun.response +// @Router /test [post] +func Fun() { + type request struct { + Name string + } + + type response struct { + Name string + Child string + } +} +` + expected := `{ + "info": { + "contact": {} + }, + "paths": { + "/test": { + "post": { + "parameters": [ + { + "description": "query params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Fun.request" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Fun.response" + } + } + } + } + } + }, + "definitions": { + "main.Fun.request": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "main.Fun.response": { + "type": "object", + "properties": { + "child": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } +}` + + f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments) + assert.NoError(t, err) + + p := New() + _ = p.packages.CollectAstFile("api", "api/api.go", f) + + _, err = p.packages.ParseTypes() + assert.NoError(t, err) + + err = p.ParseRouterAPIInfo("", f) + assert.NoError(t, err) + + b, _ := json.MarshalIndent(p.swagger, "", " ") + t.Log(string(b)) + assert.Equal(t, expected, string(b)) +} + func TestPackagesDefinitions_CollectAstFileInit(t *testing.T) { t.Parallel() diff --git a/schema.go b/schema.go index 59dbc6fe6..0baee9328 100644 --- a/schema.go +++ b/schema.go @@ -161,6 +161,30 @@ func ignoreNameOverride(name string) bool { return len(name) != 0 && name[0] == IgnoreNameOverridePrefix } +// TypeDocNameFuncScoped get alias from comment '// @name ', otherwise the original type name to display in doc. +func TypeDocNameFuncScoped(pkgName string, spec *ast.TypeSpec, fnName string) string { + if spec != nil && !ignoreNameOverride(pkgName) { + if spec.Comment != nil { + for _, comment := range spec.Comment.List { + texts := strings.Split(strings.TrimSpace(strings.TrimLeft(comment.Text, "/")), " ") + if len(texts) > 1 && strings.ToLower(texts[0]) == "@name" { + return texts[1] + } + } + } + + if spec.Name != nil { + return fullTypeNameFunctionScoped(strings.Split(pkgName, ".")[0], fnName, spec.Name.Name) + } + } + + if ignoreNameOverride(pkgName) { + return pkgName[1:] + } + + return pkgName +} + // RefSchema build a reference schema. func RefSchema(refType string) *spec.Schema { return spec.RefSchema("#/definitions/" + refType) diff --git a/schema_test.go b/schema_test.go index b5b724818..d6fb3fdb2 100644 --- a/schema_test.go +++ b/schema_test.go @@ -177,3 +177,30 @@ func TestTypeDocName(t *testing.T) { }, })) } + +func TestTypeDocNameFuncScoped(t *testing.T) { + t.Parallel() + + expected := "a/package" + assert.Equal(t, expected, TypeDocNameFuncScoped(expected, nil, "FnName")) + + expected = "package.FnName.Model" + assert.Equal(t, expected, TypeDocNameFuncScoped("package", &ast.TypeSpec{Name: &ast.Ident{Name: "Model"}}, "FnName")) + + expected = "Model" + assert.Equal(t, expected, TypeDocNameFuncScoped("package", &ast.TypeSpec{ + Comment: &ast.CommentGroup{ + List: []*ast.Comment{{Text: "// @name Model"}}, + }, + }, "FnName")) + + expected = "package.FnName.ModelName" + assert.Equal(t, expected, TypeDocNameFuncScoped("$package.FnName.ModelName", &ast.TypeSpec{Name: &ast.Ident{Name: "Model"}}, "FnName")) + + expected = "Model" + assert.Equal(t, expected, TypeDocNameFuncScoped("$Model", &ast.TypeSpec{ + Comment: &ast.CommentGroup{ + List: []*ast.Comment{{Text: "// @name ModelName"}}, + }, + }, "FnName")) +} diff --git a/testdata/duplicated_function_scoped/api/api.go b/testdata/duplicated_function_scoped/api/api.go new file mode 100644 index 000000000..25bfcc809 --- /dev/null +++ b/testdata/duplicated_function_scoped/api/api.go @@ -0,0 +1,12 @@ +package api + +import "net/http" + +// @Description get Foo +// @ID get-foo +// @Success 200 {object} api.GetFoo.response +// @Router /testapi/get-foo [get] +func GetFoo(w http.ResponseWriter, r *http.Request) { + type response struct { + } +} diff --git a/testdata/duplicated_function_scoped/main.go b/testdata/duplicated_function_scoped/main.go new file mode 100644 index 000000000..d5f34680f --- /dev/null +++ b/testdata/duplicated_function_scoped/main.go @@ -0,0 +1,22 @@ +package composition + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/duplicated_function_scoped/api" + otherapi "github.com/swaggo/swag/testdata/duplicated_function_scoped/other_api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server +// @termsOfService http://swagger.io/terms/ + +// @host petstore.swagger.io +// @BasePath /v2 + +func main() { + http.HandleFunc("/testapi/get-foo", api.GetFoo) + http.HandleFunc("/testapi/post-bar", otherapi.GetFoo) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/duplicated_function_scoped/other_api/api.go b/testdata/duplicated_function_scoped/other_api/api.go new file mode 100644 index 000000000..25bfcc809 --- /dev/null +++ b/testdata/duplicated_function_scoped/other_api/api.go @@ -0,0 +1,12 @@ +package api + +import "net/http" + +// @Description get Foo +// @ID get-foo +// @Success 200 {object} api.GetFoo.response +// @Router /testapi/get-foo [get] +func GetFoo(w http.ResponseWriter, r *http.Request) { + type response struct { + } +} diff --git a/testdata/simple/api/api.go b/testdata/simple/api/api.go index 34e843713..85a7fa48f 100644 --- a/testdata/simple/api/api.go +++ b/testdata/simple/api/api.go @@ -130,3 +130,11 @@ type SwagReturn []map[string]string func GetPet6MapString() { } + +// @Success 200 {object} api.GetPet6FunctionScopedResponse.response "ok" +// @Router /GetPet6FunctionScopedResponse [get] +func GetPet6FunctionScopedResponse() { + type response struct { + Name string + } +} diff --git a/testdata/simple/expected.json b/testdata/simple/expected.json index e3190a02b..0904d3ffa 100644 --- a/testdata/simple/expected.json +++ b/testdata/simple/expected.json @@ -101,6 +101,18 @@ } } }, + "/GetPet6FunctionScopedResponse": { + "get": { + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/api.GetPet6FunctionScopedResponse.response" + } + } + } + } + }, "/GetPet6MapString": { "get": { "responses": { @@ -389,6 +401,14 @@ } }, "definitions": { + "api.GetPet6FunctionScopedResponse.response": { + "type": "object", + "properties": { + "Name": { + "type": "string" + } + } + }, "cross.Cross": { "type": "object", "properties": { diff --git a/types.go b/types.go index 82ddbbebb..505984fdb 100644 --- a/types.go +++ b/types.go @@ -22,7 +22,8 @@ type TypeSpecDef struct { TypeSpec *ast.TypeSpec // path of package starting from under ${GOPATH}/src or from module path in go.mod - PkgPath string + PkgPath string + ParentSpec ast.Decl } // Name the name of the typeSpec. @@ -36,7 +37,13 @@ func (t *TypeSpecDef) Name() string { // FullName full name of the typeSpec. func (t *TypeSpecDef) FullName() string { - return fullTypeName(t.File.Name.Name, t.TypeSpec.Name.Name) + var fullName string + if parentFun, ok := (t.ParentSpec).(*ast.FuncDecl); ok && parentFun != nil { + fullName = fullTypeNameFunctionScoped(t.File.Name.Name, parentFun.Name.Name, t.TypeSpec.Name.Name) + } else { + fullName = fullTypeName(t.File.Name.Name, t.TypeSpec.Name.Name) + } + return fullName } // FullPath of the typeSpec.