diff --git a/README.md b/README.md index 066ba58ea..b453c411b 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ OPTIONS: --templateDelims value, --td value Provide custom delimiters for Go template generation. The format is leftDelim,rightDelim. For example: "[[,]]" --collectionFormat value, --cf value Set default collection format (default: "csv") --state value Initial state for the state machine (default: ""), @HostState in root file, @State in other files + --parseFuncBody Parse API info within body of functions in go files, disabled by default (default: false) --help, -h show help (default: false) ``` @@ -931,7 +932,7 @@ func GetPosts(w http.ResponseWriter, r *http.Request) { _ = web.GenericNestedResponse[types.Post]{} } ``` -See [this file](https://github.com/swaggo/swag/blob/master/testdata/generics_nested/api/api.go) for more details +See [this file](https://github.com/swaggo/swag/blob/master/testdata/generics_nested/api/api.go) for more details and other examples. ### Change the default Go Template action delimiters diff --git a/cmd/swag/main.go b/cmd/swag/main.go index 6c997b971..fd12c7f8a 100644 --- a/cmd/swag/main.go +++ b/cmd/swag/main.go @@ -42,6 +42,7 @@ const ( collectionFormatFlag = "collectionFormat" packagePrefixFlag = "packagePrefix" stateFlag = "state" + parseFuncBodyFlag = "parseFuncBody" ) var initFlags = []cli.Flag{ @@ -180,6 +181,11 @@ var initFlags = []cli.Flag{ Value: "", Usage: "Set host state for swagger.json", }, + &cli.BoolFlag{ + Name: parseFuncBodyFlag, + // Value: false, + Usage: "Parse API info within body of functions in go files, disabled by default (default: false)", + }, } func initAction(ctx *cli.Context) error { @@ -261,6 +267,7 @@ func initAction(ctx *cli.Context) error { CollectionFormat: collectionFormat, PackagePrefix: ctx.String(packagePrefixFlag), State: ctx.String(stateFlag), + ParseFuncBody: ctx.Bool(parseFuncBodyFlag), }) } diff --git a/gen/gen.go b/gen/gen.go index 43cf73ed8..417d51c36 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -146,6 +146,9 @@ type Config struct { // State set host state State string + + // ParseFuncBody whether swag should parse api info inside of funcs + ParseFuncBody bool } // Build builds swagger json file for given searchDir and mainAPIFile. Returns json. @@ -213,6 +216,7 @@ func (g *Gen) Build(config *Config) error { p.ParseInternal = config.ParseInternal p.RequiredByDefault = config.RequiredByDefault p.HostState = config.State + p.ParseFuncBody = config.ParseFuncBody if err := p.ParseAPIMultiSearchDir(searchDirs, config.MainAPIFile, config.ParseDepth); err != nil { return err diff --git a/parser.go b/parser.go index 8a47d0046..ff40c32df 100644 --- a/parser.go +++ b/parser.go @@ -179,6 +179,9 @@ type Parser struct { // HostState is the state of the host HostState string + + // ParseFuncBody whether swag should parse api info inside of funcs + ParseFuncBody bool } // FieldParserFactory create FieldParser. @@ -1016,30 +1019,28 @@ func matchExtension(extensionToMatch string, comments []*ast.Comment) (match boo // ParseRouterAPIInfo parses router api info for given astFile. func (parser *Parser) ParseRouterAPIInfo(fileInfo *AstFileInfo) error { -DeclsLoop: - for _, astDescription := range fileInfo.File.Decls { - if (fileInfo.ParseFlag & ParseOperations) == ParseNone { - continue + if (fileInfo.ParseFlag & ParseOperations) == ParseNone { + return nil + } + + // parse File.Comments instead of File.Decls.Doc if ParseFuncBody flag set to "true" + if parser.ParseFuncBody { + for _, astComments := range fileInfo.File.Comments { + if astComments.List != nil { + if err := parser.parseRouterAPIInfoComment(astComments.List, fileInfo); err != nil { + return err + } + } } + + return nil + } + + for _, astDescription := range fileInfo.File.Decls { astDeclaration, ok := astDescription.(*ast.FuncDecl) if ok && astDeclaration.Doc != nil && astDeclaration.Doc.List != nil { - if parser.matchTags(astDeclaration.Doc.List) && - matchExtension(parser.parseExtension, astDeclaration.Doc.List) { - // for per 'function' comment, create a new 'Operation' object - operation := NewOperation(parser, SetCodeExampleFilesDirectory(parser.codeExampleFilesDir)) - for _, comment := range astDeclaration.Doc.List { - err := operation.ParseComment(comment.Text, fileInfo.File) - if err != nil { - return fmt.Errorf("ParseComment error in file %s :%+v", fileInfo.Path, err) - } - if operation.State != "" && operation.State != parser.HostState { - continue DeclsLoop - } - } - err := processRouterOperation(parser, operation) - if err != nil { - return err - } + if err := parser.parseRouterAPIInfoComment(astDeclaration.Doc.List, fileInfo); err != nil { + return err } } } @@ -1047,6 +1048,28 @@ DeclsLoop: return nil } +func (parser *Parser) parseRouterAPIInfoComment(comments []*ast.Comment, fileInfo *AstFileInfo) error { + if parser.matchTags(comments) && matchExtension(parser.parseExtension, comments) { + // for per 'function' comment, create a new 'Operation' object + operation := NewOperation(parser, SetCodeExampleFilesDirectory(parser.codeExampleFilesDir)) + for _, comment := range comments { + err := operation.ParseComment(comment.Text, fileInfo.File) + if err != nil { + return fmt.Errorf("ParseComment error in file %s :%+v", fileInfo.Path, err) + } + if operation.State != "" && operation.State != parser.HostState { + return nil + } + } + err := processRouterOperation(parser, operation) + if err != nil { + return err + } + } + + return nil +} + func refRouteMethodOp(item *spec.PathItem, method string) (op **spec.Operation) { switch method { case http.MethodGet: diff --git a/parser_test.go b/parser_test.go index 0479a7595..c92f3dbda 100644 --- a/parser_test.go +++ b/parser_test.go @@ -3363,14 +3363,14 @@ func TestParseFunctionScopedStructDefinition(t *testing.T) { src := ` package main -// @Param request body main.Fun.request true "query params" +// @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 @@ -3395,14 +3395,14 @@ func TestParseFunctionScopedStructRequestResponseJSON(t *testing.T) { src := ` package main -// @Param request body main.Fun.request true "query params" +// @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 @@ -4123,3 +4123,66 @@ func TestParser_skipPackageByPrefix(t *testing.T) { assert.False(t, parser.skipPackageByPrefix("github.com/swaggo/swag/cmd")) assert.False(t, parser.skipPackageByPrefix("github.com/swaggo/swag/gen")) } + +func TestParser_ParseRouterApiInFuncBody(t *testing.T) { + t.Parallel() + + src := ` +package test + +func Test(){ + // @Router /api/{id} [get] + _ = func() { + } +} +` + p := New() + p.ParseFuncBody = true + err := p.packages.ParseFile("api", "api/api.go", src, ParseAll) + assert.NoError(t, err) + + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) + assert.NoError(t, err) + + ps := p.swagger.Paths.Paths + + val, ok := ps["/api/{id}"] + + assert.True(t, ok) + assert.NotNil(t, val.Get) +} + +func TestParser_ParseRouterApiInfoInAndOutFuncBody(t *testing.T) { + t.Parallel() + + src := ` +package test + +// @Router /api/outside [get] +func otherRoute(){ +} + +func Test(){ + // @Router /api/inside [get] + _ = func() { + } +} +` + p := New() + p.ParseFuncBody = true + err := p.packages.ParseFile("api", "api/api.go", src, ParseAll) + assert.NoError(t, err) + + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) + assert.NoError(t, err) + + ps := p.swagger.Paths.Paths + + val1, ok := ps["/api/outside"] + assert.True(t, ok) + assert.NotNil(t, val1.Get) + + val2, ok := ps["/api/inside"] + assert.True(t, ok) + assert.NotNil(t, val2.Get) +}