From 33441d9b26158666157d08e33862277297b3cf99 Mon Sep 17 00:00:00 2001 From: Nerzal Date: Wed, 5 Apr 2023 18:19:12 +0200 Subject: [PATCH 01/34] Implementing OpenAPI 3.1.0 spec (#1513) * fix x-tagGroups * fix module name * change paths * refactoring * update dependencies * stuff * add log * fix finding of main file * fix broken type resolution * fix bug * clean deps * fix tool after merging upstream * use json-iterator to marshal json * fix generating of json examples * update config used by jsoniter * bump version * update dependencies * resolve merge conflicts * use newest go in docker * yep * fix gen * fix gen * update swag version * yep * fix parser * fix some tests * fix all tests * parse most of general api description * implement security scheme parsing * parse oauth2 specs * parse scopes and extensions in security schemes * extend parsing security stuff * process v3 routes * meh * find unimported types * parse basic operation info * parse primitive and object parameters * generate openapi spec * fix module name * cleanup * update version to 2.0 * fix issues that appread after merging * cleanup after merge conflicts * fix all tests * add go 1.19 to workflows * pin dockerfile to 1.19.7 * Set minimum supported Go version to 1.18.x * parse response headers * copy readme * started to implement field parser * Refactor: use RefOrSpec instead of Spec * start to add tests for operationv3 * fix tests * implement allOf with primitive types * Add NestedPrimitiveArrayType test * implement TestParseResponseCommentWithNestedFieldsV3 * add more tests * parse arrays and maps * fix implementation of map types * implement more tests * fix example docs * adjust example * fix example jsons * support array types in Parameters * implement more tests, implement correct collectionFormat handling * finish implementation of operationv3 tests * all tests green * fix parsing of security definitions * add test for generalAPI info * end of day checkin * Update example.json * fix codeSamples from file and fix creation of operations * fix resolving of schema ref errors * fix tests that broke due to fixes on model parsing * Fix creating schemes of array types of custom objects * Fix resolution of refSchemas * cleanup * update dependencies * cleanup * Update README.md reset readme.md * Update README_zh-CN.md reset readme_zh-CN * update dependency * reset test file --------- Co-authored-by: Tobias Theel --- .github/workflows/ci.yml | 2 +- .gitignore | 3 + README.md | 2 +- README_zh-CN.md | 2 +- cmd/swag/main.go | 12 +- enums_test.go | 35 +- example/celler/controller/bottles.go | 1 - example/markdown/go.sum | 19 +- example/markdown/main.go | 29 +- example/object-map-example/go.mod | 8 +- example/object-map-example/go.sum | 20 +- extensions.go | 4 + field_parser.go | 6 + field_parserv3.go | 572 ++++++++ gen/gen.go | 66 +- gen/gen_test.go | 1 - gen/genv3.go | 202 +++ genericsv3.go | 34 + go.mod | 40 +- go.sum | 91 +- operation.go | 37 +- operation_test.go | 13 +- operationv3.go | 993 +++++++++++++ operationv3_test.go | 1916 +++++++++++++++++++++++++ packages.go | 42 +- packages_test.go | 2 +- parser.go | 318 ++-- parser_test.go | 2 +- parserv3.go | 995 +++++++++++++ parserv3_test.go | 369 +++++ schemav3.go | 141 ++ testdata/code_examples/example.json | 10 +- testdata/generics_property/api/api.go | 3 +- testdata/v3/extensionsFail1.go | 9 + testdata/v3/extensionsFail2.go | 9 + testdata/v3/main.go | 64 + testdata/v3/pet/main.go | 20 + testdata/v3/pet/web/handler.go | 38 + testdata/v3/simple/api/api.go | 140 ++ testdata/v3/simple/cross/test.go | 6 + testdata/v3/simple/main.go | 28 + testdata/v3/simple/web/handler.go | 101 ++ typesv3.go | 10 + version.go | 2 +- 44 files changed, 6140 insertions(+), 277 deletions(-) create mode 100644 extensions.go create mode 100644 field_parserv3.go create mode 100644 gen/genv3.go create mode 100644 genericsv3.go create mode 100644 operationv3.go create mode 100644 operationv3_test.go create mode 100644 parserv3.go create mode 100644 parserv3_test.go create mode 100644 schemav3.go create mode 100644 testdata/v3/extensionsFail1.go create mode 100644 testdata/v3/extensionsFail2.go create mode 100644 testdata/v3/main.go create mode 100644 testdata/v3/pet/main.go create mode 100644 testdata/v3/pet/web/handler.go create mode 100644 testdata/v3/simple/api/api.go create mode 100644 testdata/v3/simple/cross/test.go create mode 100644 testdata/v3/simple/main.go create mode 100644 testdata/v3/simple/web/handler.go create mode 100644 typesv3.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d984775be..9e129e778 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: test: strategy: matrix: - go: [ '1.16.x', '1.17.x', '1.18.x' ] + go: [ '1.18.x', '1.19.x', '1.20.x' ] platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/.gitignore b/.gitignore index 6ba1ec223..2438076f8 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ cover.out /swag /swag.exe +cmd/swag/docs/* + +.vscode/launch.json \ No newline at end of file diff --git a/README.md b/README.md index c246128cc..4a48fa6ff 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a varie ```sh go install github.com/swaggo/swag/cmd/swag@latest ``` -To build from source you need [Go](https://golang.org/dl/) (1.16 or newer). +To build from source you need [Go](https://golang.org/dl/) (1.17 or newer). Or download a pre-compiled binary from the [release page](https://github.com/swaggo/swag/releases). diff --git a/README_zh-CN.md b/README_zh-CN.md index 0c44de416..60b6fb559 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -50,7 +50,7 @@ Swag将Go的注释转换为Swagger2.0文档。我们为流行的 [Go Web Framewo go install github.com/swaggo/swag/cmd/swag@latest ``` -从源码开始构建的话,需要有Go环境(1.16及以上版本)。 +从源码开始构建的话,需要有Go环境(1.17及以上版本)。 或者从github的release页面下载预编译好的二进制文件。 diff --git a/cmd/swag/main.go b/cmd/swag/main.go index 1b325865e..f2164e10d 100644 --- a/cmd/swag/main.go +++ b/cmd/swag/main.go @@ -7,11 +7,11 @@ import ( "os" "strings" - "github.com/urfave/cli/v2" - "github.com/swaggo/swag" "github.com/swaggo/swag/format" "github.com/swaggo/swag/gen" + + "github.com/urfave/cli/v2" ) const ( @@ -35,6 +35,7 @@ const ( quietFlag = "quiet" tagsFlag = "tags" parseExtensionFlag = "parseExtension" + openAPIVersionFlag = "v3.1" packageName = "packageName" collectionFormatFlag = "collectionFormat" ) @@ -143,6 +144,11 @@ var initFlags = []cli.Flag{ Value: "", Usage: "A comma-separated list of tags to filter the APIs for which the documentation is generated.Special case if the tag is prefixed with the '!' character then the APIs with that tag will be excluded", }, + &cli.BoolFlag{ + Name: openAPIVersionFlag, + Value: false, + Usage: "Generate OpenAPI V3.1 spec", + }, &cli.StringFlag{ Name: packageName, Value: "", @@ -201,11 +207,13 @@ func initAction(ctx *cli.Context) error { Tags: ctx.String(tagsFlag), PackageName: ctx.String(packageName), Debugger: logger, + OpenAPIVersion: ctx.Bool(openAPIVersionFlag), CollectionFormat: collectionFormat, }) } func main() { + fmt.Println("Swag version: ", swag.Version) app := cli.NewApp() app.Version = swag.Version app.Usage = "Automatically generate RESTful API documentation with Swagger 2.0 for Go." diff --git a/enums_test.go b/enums_test.go index dbae10d83..1d8e1930f 100644 --- a/enums_test.go +++ b/enums_test.go @@ -1,32 +1,29 @@ package swag import ( - "encoding/json" - "os" - "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseGlobalEnums(t *testing.T) { searchDir := "testdata/enums" - expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) - assert.NoError(t, err) p := New() - 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)) - constsPath := "github.com/swaggo/swag/testdata/enums/consts" - assert.Equal(t, 64, p.packages.packages[constsPath].ConstTable["uintSize"].Value) - assert.Equal(t, int32(62), p.packages.packages[constsPath].ConstTable["maxBase"].Value) - assert.Equal(t, 8, p.packages.packages[constsPath].ConstTable["shlByLen"].Value) - assert.Equal(t, 255, p.packages.packages[constsPath].ConstTable["hexnum"].Value) - assert.Equal(t, 15, p.packages.packages[constsPath].ConstTable["octnum"].Value) - assert.Equal(t, `aa\nbb\u8888cc`, p.packages.packages[constsPath].ConstTable["nonescapestr"].Value) - assert.Equal(t, "aa\nbb\u8888cc", p.packages.packages[constsPath].ConstTable["escapestr"].Value) - assert.Equal(t, '\u8888', p.packages.packages[constsPath].ConstTable["escapechar"].Value) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + require.NoError(t, err) + + const constsPath = "github.com/swaggo/swag/testdata/enums/consts" + table := p.packages.packages[constsPath].ConstTable + require.NotNil(t, table, "const table must not be nil") + + assert.Equal(t, 64, table["uintSize"].Value) + assert.Equal(t, int32(62), table["maxBase"].Value) + assert.Equal(t, 8, table["shlByLen"].Value) + assert.Equal(t, 255, table["hexnum"].Value) + assert.Equal(t, 15, table["octnum"].Value) + assert.Equal(t, `aa\nbb\u8888cc`, table["nonescapestr"].Value) + assert.Equal(t, "aa\nbb\u8888cc", table["escapestr"].Value) + assert.Equal(t, '\u8888', table["escapechar"].Value) } diff --git a/example/celler/controller/bottles.go b/example/celler/controller/bottles.go index 925414d32..1a7907d5c 100644 --- a/example/celler/controller/bottles.go +++ b/example/celler/controller/bottles.go @@ -5,7 +5,6 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/swaggo/swag/example/celler/httputil" "github.com/swaggo/swag/example/celler/model" ) diff --git a/example/markdown/go.sum b/example/markdown/go.sum index b08a39e07..cb3034a03 100644 --- a/example/markdown/go.sum +++ b/example/markdown/go.sum @@ -67,15 +67,12 @@ github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pA github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -83,12 +80,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -97,26 +92,20 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= diff --git a/example/markdown/main.go b/example/markdown/main.go index a13720c27..674fbdf8e 100644 --- a/example/markdown/main.go +++ b/example/markdown/main.go @@ -1,31 +1,30 @@ package main import ( - "net/http" - "github.com/gorilla/mux" httpSwagger "github.com/swaggo/http-swagger" "github.com/swaggo/swag/example/markdown/api" _ "github.com/swaggo/swag/example/markdown/docs" + "net/http" ) -// @title Swagger Example API -// @version 1.0 -// @description This is a sample server Petstore server. -// @description.markdown -// @termsOfService http://swagger.io/terms/ +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @description.markdown +// @termsOfService http://swagger.io/terms/ -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @tag.name admin -// @tag.description.markdown +// @tag.name admin +// @tag.description.markdown -// @BasePath /v2 +// @BasePath /v2 func main() { router := mux.NewRouter() diff --git a/example/object-map-example/go.mod b/example/object-map-example/go.mod index 1c8b2e5ee..57995e45f 100644 --- a/example/object-map-example/go.mod +++ b/example/object-map-example/go.mod @@ -25,12 +25,12 @@ require ( github.com/leodido/go-urn v1.2.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.12 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect - github.com/ugorji/go/codec v1.1.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/ugorji/go/codec v1.1.13 // indirect golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.5.0 // indirect - golang.org/x/tools v0.1.10 // indirect + golang.org/x/tools v0.1.12 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/example/object-map-example/go.sum b/example/object-map-example/go.sum index ff15136db..bac00b11c 100644 --- a/example/object-map-example/go.sum +++ b/example/object-map-example/go.sum @@ -61,10 +61,12 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= @@ -92,13 +94,14 @@ github.com/swaggo/gin-swagger v1.4.2/go.mod h1:hmJ1vPn+XjUvnbzjCdUAxVqgraxELxk8x github.com/swaggo/swag v1.7.9/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU= github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go v1.1.13 h1:nB3O5kBSQGjEQAcfe1aLUYuxmXdFKmYgBZhY32rQb6Q= +github.com/ugorji/go v1.1.13/go.mod h1:jxau1n+/wyTGLQoCkjok9r5zFa/FxT6eI5HiHKQszjc= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.1.13 h1:013LbFhocBoIqgHeIHKlV4JWYhqogATYWZhIcH0WHn4= +github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -107,8 +110,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -117,7 +118,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -134,7 +134,6 @@ golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -153,13 +152,10 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/extensions.go b/extensions.go new file mode 100644 index 000000000..358104cac --- /dev/null +++ b/extensions.go @@ -0,0 +1,4 @@ +package swag + +// CodeSamples is used to parse code samples. +type CodeSamples []map[string]string diff --git a/field_parser.go b/field_parser.go index 98ad0ddc7..e317ea9b6 100644 --- a/field_parser.go +++ b/field_parser.go @@ -21,6 +21,7 @@ const ( swaggerTypeTag = "swaggertype" swaggerIgnoreTag = "swaggerignore" ) +var _ FieldParser = &tagBaseFieldParser{} type tagBaseFieldParser struct { p *Parser @@ -519,6 +520,7 @@ func (ps *tagBaseFieldParser) IsRequired() (bool, error) { } func parseValidTags(validTag string, sf *structField) { + // `validate:"required,max=10,min=1"` // ps. required checked by IsRequired(). for _, val := range strings.Split(validTag, ",") { @@ -541,6 +543,10 @@ func parseValidTags(validTag string, sf *structField) { case "min", "gte": sf.setMin(valValue) case "oneof": + if strings.Contains(validTag, "swaggerIgnore") { + continue + } + sf.setOneOf(valValue) case "unique": if sf.schemaType == ARRAY { diff --git a/field_parserv3.go b/field_parserv3.go new file mode 100644 index 000000000..068067be8 --- /dev/null +++ b/field_parserv3.go @@ -0,0 +1,572 @@ +package swag + +import ( + "fmt" + "go/ast" + "reflect" + "strconv" + "strings" + + "github.com/sv-tools/openapi/spec" +) + +type structFieldV3 struct { + schemaType string + arrayType string + formatType string + maximum *int + minimum *int + multipleOf *int + maxLength *int + minLength *int + maxItems *int + minItems *int + exampleValue interface{} + enums []interface{} + enumVarNames []interface{} + unique bool +} + +func (sf *structFieldV3) setOneOf(valValue string) { + if len(sf.enums) != 0 { + return + } + + enumType := sf.schemaType + if sf.schemaType == ARRAY { + enumType = sf.arrayType + } + + valValues := parseOneOfParam2(valValue) + for i := range valValues { + value, err := defineType(enumType, valValues[i]) + if err != nil { + continue + } + + sf.enums = append(sf.enums, value) + } +} + +func (sf *structFieldV3) setMin(valValue string) { + value, err := strconv.Atoi(valValue) + if err != nil { + return + } + + switch sf.schemaType { + case INTEGER, NUMBER: + sf.minimum = &value + case STRING: + sf.minLength = &value + case ARRAY: + sf.minItems = &value + } +} + +func (sf *structFieldV3) setMax(valValue string) { + value, err := strconv.Atoi(valValue) + if err != nil { + return + } + + switch sf.schemaType { + case INTEGER, NUMBER: + sf.maximum = &value + case STRING: + sf.maxLength = &value + case ARRAY: + sf.maxItems = &value + } +} + +type tagBaseFieldParserV3 struct { + p *Parser + field *ast.Field + tag reflect.StructTag +} + +func newTagBaseFieldParserV3(p *Parser, field *ast.Field) FieldParserV3 { + fieldParser := tagBaseFieldParserV3{ + p: p, + field: field, + tag: "", + } + if fieldParser.field.Tag != nil { + fieldParser.tag = reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", "")) + } + + return &fieldParser +} + +func (ps *tagBaseFieldParserV3) CustomSchema() (*spec.RefOrSpec[spec.Schema], error) { + if ps.field.Tag == nil { + return nil, nil + } + + typeTag := ps.tag.Get(swaggerTypeTag) + if typeTag != "" { + return BuildCustomSchemaV3(strings.Split(typeTag, ",")) + } + + return nil, nil +} + +// ComplementSchema complement schema with field properties +func (ps *tagBaseFieldParserV3) ComplementSchema(schema *spec.RefOrSpec[spec.Schema]) error { + if schema.Spec == nil { + schema = ps.p.openAPI.Components.Spec.Schemas[strings.ReplaceAll(schema.Ref.Ref, "#/components/schemas/", "")] + if schema == nil { + return fmt.Errorf("could not resolve schema for ref %s", schema.Ref.Ref) + } + } + + types := ps.p.GetSchemaTypePathV3(schema, 2) + if len(types) == 0 { + return fmt.Errorf("invalid type for field: %s", ps.field.Names[0]) + } + + if schema.Ref != nil { //IsRefSchema(schema) + // TODO fetch existing schema from components + var newSchema = spec.Schema{} + err := ps.complementSchema(&newSchema, types) + if err != nil { + return err + } + // if !reflect.ValueOf(newSchema).IsZero() { + // *schema = *(newSchema.WithAllOf(*schema.Spec)) + // } + return nil + } + + return ps.complementSchema(schema.Spec, types) +} + +// complementSchema complement schema with field properties +func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []string) error { + if ps.field.Tag == nil { + if ps.field.Doc != nil { + schema.Description = strings.TrimSpace(ps.field.Doc.Text()) + } + + if schema.Description == "" && ps.field.Comment != nil { + schema.Description = strings.TrimSpace(ps.field.Comment.Text()) + } + + return nil + } + + field := &structFieldV3{ + schemaType: types[0], + formatType: ps.tag.Get(formatTag), + } + + if len(types) > 1 && (types[0] == ARRAY || types[0] == OBJECT) { + field.arrayType = types[1] + } + + jsonTagValue := ps.tag.Get(jsonTag) + + bindingTagValue := ps.tag.Get(bindingTag) + if bindingTagValue != "" { + field.parseValidTags(bindingTagValue) + } + + validateTagValue := ps.tag.Get(validateTag) + if validateTagValue != "" { + field.parseValidTags(validateTagValue) + } + + enumsTagValue := ps.tag.Get(enumsTag) + if enumsTagValue != "" { + err := field.parseEnumTags(enumsTagValue) + if err != nil { + return err + } + } + + if IsNumericType(field.schemaType) || IsNumericType(field.arrayType) { + maximum, err := getIntTagV3(ps.tag, maximumTag) + if err != nil { + return err + } + + if maximum != nil { + field.maximum = maximum + } + + minimum, err := getIntTagV3(ps.tag, minimumTag) + if err != nil { + return err + } + + if minimum != nil { + field.minimum = minimum + } + + multipleOf, err := getIntTagV3(ps.tag, multipleOfTag) + if err != nil { + return err + } + + if multipleOf != nil { + field.multipleOf = multipleOf + } + } + + if field.schemaType == STRING || field.arrayType == STRING { + maxLength, err := getIntTagV3(ps.tag, maxLengthTag) + if err != nil { + return err + } + + if maxLength != nil { + field.maxLength = maxLength + } + + minLength, err := getIntTagV3(ps.tag, minLengthTag) + if err != nil { + return err + } + + if minLength != nil { + field.minLength = minLength + } + } + + // json:"name,string" or json:",string" + exampleTagValue, ok := ps.tag.Lookup(exampleTag) + if ok { + field.exampleValue = exampleTagValue + + if !strings.Contains(jsonTagValue, ",string") { + example, err := defineTypeOfExample(field.schemaType, field.arrayType, exampleTagValue) + if err != nil { + return err + } + + field.exampleValue = example + } + } + + // perform this after setting everything else (min, max, etc...) + if strings.Contains(jsonTagValue, ",string") { + // @encoding/json: "It applies only to fields of string, floating point, integer, or boolean types." + defaultValues := map[string]string{ + // Zero Values as string + STRING: "", + INTEGER: "0", + BOOLEAN: "false", + NUMBER: "0", + } + + defaultValue, ok := defaultValues[field.schemaType] + if ok { + field.schemaType = STRING + *schema = *PrimitiveSchemaV3(field.schemaType).Spec + + if field.exampleValue == nil { + // if exampleValue is not defined by the user, + // we will force an example with a correct value + // (eg: int->"0", bool:"false") + field.exampleValue = defaultValue + } + } + } + + if ps.field.Doc != nil { + schema.Description = strings.TrimSpace(ps.field.Doc.Text()) + } + + if schema.Description == "" && ps.field.Comment != nil { + schema.Description = strings.TrimSpace(ps.field.Comment.Text()) + } + + schema.ReadOnly = ps.tag.Get(readOnlyTag) == "true" + + defaultTagValue := ps.tag.Get(defaultTag) + if defaultTagValue != "" { + value, err := defineType(field.schemaType, defaultTagValue) + if err != nil { + return err + } + + schema.Default = value + } + + schema.Example = field.exampleValue + + if field.schemaType != ARRAY { + schema.Format = field.formatType + } + + extensionsTagValue := ps.tag.Get(extensionsTag) + if extensionsTagValue != "" { + schema.Extensions = setExtensionParam(extensionsTagValue) + } + + varNamesTag := ps.tag.Get("x-enum-varnames") + if varNamesTag != "" { + varNames := strings.Split(varNamesTag, ",") + if len(varNames) != len(field.enums) { + return fmt.Errorf("invalid count of x-enum-varnames. expected %d, got %d", len(field.enums), len(varNames)) + } + + field.enumVarNames = nil + + for _, v := range varNames { + field.enumVarNames = append(field.enumVarNames, v) + } + + if field.schemaType == ARRAY { + // Add the var names in the items schema + if schema.Items.Schema.Spec.Extensions == nil { + schema.Items.Schema.Spec.Extensions = map[string]interface{}{} + } + schema.Items.Schema.Spec.Extensions[enumVarNamesExtension] = field.enumVarNames + } else { + // Add to top level schema + if schema.Extensions == nil { + schema.Extensions = map[string]interface{}{} + } + schema.Extensions[enumVarNamesExtension] = field.enumVarNames + } + } + + elemSchema := schema + + if field.schemaType == ARRAY { + // For Array only + schema.MaxItems = field.maxItems + schema.MinItems = field.minItems + schema.UniqueItems = &field.unique + + elemSchema = schema.Items.Schema.Spec + if elemSchema == nil { + elemSchema = ps.p.getSchemaByRef(schema.Items.Schema.Ref) + } + + elemSchema.Format = field.formatType + } + + elemSchema.Maximum = field.maximum + elemSchema.Minimum = field.minimum + elemSchema.MultipleOf = field.multipleOf + elemSchema.MaxLength = field.maxLength + elemSchema.MinLength = field.minLength + elemSchema.Enum = field.enums + + return nil +} + +func getIntTagV3(structTag reflect.StructTag, tagName string) (*int, error) { + strValue := structTag.Get(tagName) + if strValue == "" { + return nil, nil + } + + value, err := strconv.Atoi(strValue) + if err != nil { + return nil, fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err) + } + + return &value, nil +} + +func parseValidTagsV3(validTag string, sf *structFieldV3) { + + // `validate:"required,max=10,min=1"` + // ps. required checked by IsRequired(). + for _, val := range strings.Split(validTag, ",") { + var ( + valValue string + keyVal = strings.Split(val, "=") + ) + + switch len(keyVal) { + case 1: + case 2: + valValue = strings.ReplaceAll(strings.ReplaceAll(keyVal[1], utf8HexComma, ","), utf8Pipe, "|") + default: + continue + } + + switch keyVal[0] { + case "max", "lte": + sf.setMax(valValue) + case "min", "gte": + sf.setMin(valValue) + case "oneof": + if strings.Contains(validTag, "swaggerIgnore") { + continue + } + + sf.setOneOf(valValue) + case "unique": + if sf.schemaType == ARRAY { + sf.unique = true + } + case "dive": + // ignore dive + return + default: + continue + } + } +} + +func (sf *structFieldV3) parseValidTags(validTag string) { + + // `validate:"required,max=10,min=1"` + // ps. required checked by IsRequired(). + for _, val := range strings.Split(validTag, ",") { + var ( + valValue string + keyVal = strings.Split(val, "=") + ) + + switch len(keyVal) { + case 1: + case 2: + valValue = strings.ReplaceAll(strings.ReplaceAll(keyVal[1], utf8HexComma, ","), utf8Pipe, "|") + default: + continue + } + + switch keyVal[0] { + case "max", "lte": + sf.setMax(valValue) + case "min", "gte": + sf.setMin(valValue) + case "oneof": + if strings.Contains(validTag, "swaggerIgnore") { + continue + } + + sf.setOneOf(valValue) + case "unique": + if sf.schemaType == ARRAY { + sf.unique = true + } + case "dive": + // ignore dive + return + default: + continue + } + } +} + +func (field *structFieldV3) parseEnumTags(enumTag string) error { + enumType := field.schemaType + if field.schemaType == ARRAY { + enumType = field.arrayType + } + + field.enums = nil + + for _, e := range strings.Split(enumTag, ",") { + value, err := defineType(enumType, e) + if err != nil { + return err + } + + field.enums = append(field.enums, value) + } + + return nil +} + +func (ps *tagBaseFieldParserV3) ShouldSkip() bool { + // Skip non-exported fields. + if ps.field.Names != nil && !ast.IsExported(ps.field.Names[0].Name) { + return true + } + + if ps.field.Tag == nil { + return false + } + + ignoreTag := ps.tag.Get(swaggerIgnoreTag) + if strings.EqualFold(ignoreTag, "true") { + return true + } + + // json:"tag,hoge" + name := strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0]) + if name == "-" { + return true + } + + return false +} + +func (ps *tagBaseFieldParserV3) FieldName() (string, error) { + var name string + + if ps.field.Tag != nil { + // json:"tag,hoge" + name = strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0]) + if name != "" { + return name, nil + } + + // use "form" tag over json tag + name = ps.FormName() + if name != "" { + return name, nil + } + } + + if ps.field.Names == nil { + return "", nil + } + + switch ps.p.PropNamingStrategy { + case SnakeCase: + return toSnakeCase(ps.field.Names[0].Name), nil + case PascalCase: + return ps.field.Names[0].Name, nil + default: + return toLowerCamelCase(ps.field.Names[0].Name), nil + } +} + +func (ps *tagBaseFieldParserV3) FormName() string { + if ps.field.Tag != nil { + return strings.TrimSpace(strings.Split(ps.tag.Get(formTag), ",")[0]) + } + return "" +} + +func (ps *tagBaseFieldParserV3) IsRequired() (bool, error) { + if ps.field.Tag == nil { + return false, nil + } + + bindingTag := ps.tag.Get(bindingTag) + if bindingTag != "" { + for _, val := range strings.Split(bindingTag, ",") { + switch val { + case requiredLabel: + return true, nil + case optionalLabel: + return false, nil + } + } + } + + validateTag := ps.tag.Get(validateTag) + if validateTag != "" { + for _, val := range strings.Split(validateTag, ",") { + switch val { + case requiredLabel: + return true, nil + case optionalLabel: + return false, nil + } + } + } + + return ps.p.RequiredByDefault, nil +} diff --git a/gen/gen.go b/gen/gen.go index ed52ac12d..5950ae345 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -15,8 +15,11 @@ import ( "text/template" "time" + jsoniter "github.com/json-iterator/go" + "github.com/ghodss/yaml" "github.com/go-openapi/spec" + openapi "github.com/sv-tools/openapi/spec" "github.com/swaggo/swag" ) @@ -29,11 +32,12 @@ type genTypeWriter func(*Config, *spec.Swagger) error // Gen presents a generate tool for swag. type Gen struct { - json func(data interface{}) ([]byte, error) - jsonIndent func(data interface{}) ([]byte, error) - jsonToYAML func(data []byte) ([]byte, error) - outputTypeMap map[string]genTypeWriter - debug Debugger + json func(data interface{}) ([]byte, error) + jsonIndent func(data interface{}) ([]byte, error) + jsonToYAML func(data []byte) ([]byte, error) + outputTypeMap map[string]genTypeWriter + outputTypeMapV3 map[string]openAPITypeWriter + debug Debugger } // Debugger is the interface that wraps the basic Printf method. @@ -46,7 +50,8 @@ func New() *Gen { gen := Gen{ json: json.Marshal, jsonIndent: func(data interface{}) ([]byte, error) { - return json.MarshalIndent(data, "", " ") + var json = jsoniter.ConfigCompatibleWithStandardLibrary + return json.MarshalIndent(&data, "", " ") }, jsonToYAML: yaml.JSONToYAML, debug: log.New(os.Stdout, "", log.LstdFlags), @@ -59,6 +64,13 @@ func New() *Gen { "yml": gen.writeYAMLSwagger, } + gen.outputTypeMapV3 = map[string]openAPITypeWriter{ + "go": gen.writeDocOpenAPI, + "json": gen.writeJSONOpenAPI, + "yaml": gen.writeYAMLOpenAPI, + "yml": gen.writeYAMLOpenAPI, + } + return &gen } @@ -127,6 +139,8 @@ type Config struct { // include only tags mentioned when searching, comma separated Tags string + // if true, OpenAPI V3.1 spec will be generated + OpenAPIVersion bool // PackageName defines package name of generated `docs.go` PackageName string @@ -182,6 +196,7 @@ func (g *Gen) Build(config *Config) error { swag.SetOverrides(overrides), swag.ParseUsingGoList(config.ParseGoList), swag.SetTags(config.Tags), + swag.SetOpenAPIVersion(config.OpenAPIVersion), swag.SetCollectionFormat(config.CollectionFormat), ) @@ -194,12 +209,45 @@ func (g *Gen) Build(config *Config) error { return err } - swagger := p.GetSwagger() - if err := os.MkdirAll(config.OutputDir, os.ModePerm); err != nil { return err } + if config.OpenAPIVersion { + openAPI := p.GetOpenAPI() + err := g.writeOpenAPI(config, openAPI) + if err != nil { + return err + } + + return nil + } + + swagger := p.GetSwagger() + err := g.writeSwagger(config, swagger) + if err != nil { + return err + } + + return nil +} + +func (g *Gen) writeOpenAPI(config *Config, o *openapi.OpenAPI) error { + for _, outputType := range config.OutputTypes { + outputType = strings.ToLower(strings.TrimSpace(outputType)) + if typeWriter, ok := g.outputTypeMapV3[outputType]; ok { + if err := typeWriter(config, o); err != nil { + return err + } + } else { + log.Printf("output type '%s' not supported", outputType) + } + } + + return nil +} + +func (g *Gen) writeSwagger(config *Config, swagger *spec.Swagger) error { for _, outputType := range config.OutputTypes { outputType = strings.ToLower(strings.TrimSpace(outputType)) if typeWriter, ok := g.outputTypeMap[outputType]; ok { @@ -464,7 +512,7 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swa var packageTemplate = `// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT. -package {{.PackageName}} +package docs import "github.com/swaggo/swag" diff --git a/gen/gen_test.go b/gen/gen_test.go index 35acd6bf3..62a313e6a 100644 --- a/gen/gen_test.go +++ b/gen/gen_test.go @@ -224,7 +224,6 @@ func TestGen_BuildDescriptionWithQuotes(t *testing.T) { require.NoError(t, err) } } - cmd := exec.Command("go", "build", "-buildmode=plugin", "github.com/swaggo/swag/testdata/quotes") cmd.Dir = config.SearchDir diff --git a/gen/genv3.go b/gen/genv3.go new file mode 100644 index 000000000..8010660f2 --- /dev/null +++ b/gen/genv3.go @@ -0,0 +1,202 @@ +package gen + +import ( + "bytes" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + "text/template" + "time" + + "github.com/sv-tools/openapi/spec" + "github.com/swaggo/swag" +) + +type openAPITypeWriter func(*Config, *spec.OpenAPI) error + +func (g *Gen) writeDocOpenAPI(config *Config, openAPI *spec.OpenAPI) error { + var filename = "docs.go" + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + docFileName := path.Join(config.OutputDir, filename) + + absOutputDir, err := filepath.Abs(config.OutputDir) + if err != nil { + return err + } + + packageName := filepath.Base(absOutputDir) + + docs, err := os.Create(docFileName) + if err != nil { + return err + } + defer docs.Close() + + // Write doc + err = g.writeGoDocV3(packageName, docs, openAPI, config) + if err != nil { + return err + } + + g.debug.Printf("create docs.go at %+v", docFileName) + + return nil +} + +func (g *Gen) writeJSONOpenAPI(config *Config, swagger *spec.OpenAPI) error { + var filename = "swagger.json" + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + jsonFileName := path.Join(config.OutputDir, filename) + + b, err := g.jsonIndent(swagger) + if err != nil { + return err + } + + err = g.writeFile(b, jsonFileName) + if err != nil { + return err + } + + g.debug.Printf("create swagger.json at %+v", jsonFileName) + + return nil +} + +func (g *Gen) writeYAMLOpenAPI(config *Config, swagger *spec.OpenAPI) error { + var filename = "swagger.yaml" + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + yamlFileName := path.Join(config.OutputDir, filename) + + b, err := g.json(swagger) + if err != nil { + return err + } + + y, err := g.jsonToYAML(b) + if err != nil { + return fmt.Errorf("cannot covert json to yaml error: %s", err) + } + + err = g.writeFile(y, yamlFileName) + if err != nil { + return err + } + + g.debug.Printf("create swagger.yaml at %+v", yamlFileName) + + return nil +} + +func (g *Gen) writeGoDocV3(packageName string, output io.Writer, openAPI *spec.OpenAPI, config *Config) error { + generator, err := template.New("swagger_info").Funcs(template.FuncMap{ + "printDoc": func(v string) string { + // Add schemes + v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:] + // Sanitize backticks + return strings.Replace(v, "`", "`+\"`\"+`", -1) + }, + }).Parse(packageTemplateV3) + if err != nil { + return err + } + + openAPISpec := spec.OpenAPI{ + Components: openAPI.Components, + OpenAPI: openAPI.OpenAPI, + Info: &spec.Extendable[spec.Info]{ + Spec: &spec.Info{ + Description: "{{escape .Description}}", + Title: "{{.Title}}", + Version: "{{.Version}}", + TermsOfService: openAPI.Info.Spec.TermsOfService, + Contact: openAPI.Info.Spec.Contact, + License: openAPI.Info.Spec.License, + Summary: openAPI.Info.Spec.Summary, + }, + Extensions: openAPI.Info.Extensions, + }, + ExternalDocs: openAPI.ExternalDocs, + Paths: openAPI.Paths, + WebHooks: openAPI.WebHooks, + JsonSchemaDialect: openAPI.JsonSchemaDialect, + Security: openAPI.Security, + Tags: openAPI.Tags, + Servers: openAPI.Servers, + } + + // crafted docs.json + buf, err := g.jsonIndent(openAPISpec) + if err != nil { + return err + } + + buffer := &bytes.Buffer{} + + err = generator.Execute(buffer, struct { + Timestamp time.Time + Doc string + PackageName string + Title string + Description string + Version string + InstanceName string + GeneratedTime bool + }{ + Timestamp: time.Now(), + GeneratedTime: config.GeneratedTime, + Doc: string(buf), + PackageName: packageName, + Title: openAPI.Info.Spec.Title, + Description: openAPI.Info.Spec.Description, + Version: openAPI.Info.Spec.Version, + InstanceName: config.InstanceName, + }) + if err != nil { + return err + } + + code := g.formatSource(buffer.Bytes()) + + // write + _, err = output.Write(code) + + return err +} + +var packageTemplateV3 = `// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT + +package docs + +import "github.com/swaggo/swag" + +const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = ` + "`{{ printDoc .Doc}}`" + ` + +// SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} holds exported Swagger Info so clients can modify it +var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = &swag.Spec{ + Version: {{ printf "%q" .Version}}, + Title: {{ printf "%q" .Title}}, + Description: {{ printf "%q" .Description}}, + InfoInstanceName: {{ printf "%q" .InstanceName }}, + SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, +} + +func init() { + swag.Register(SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}) +} +` diff --git a/genericsv3.go b/genericsv3.go new file mode 100644 index 000000000..1306a3622 --- /dev/null +++ b/genericsv3.go @@ -0,0 +1,34 @@ +package swag + +import ( + "go/ast" + + "github.com/sv-tools/openapi/spec" +) + +func (p *Parser) parseGenericTypeExprV3(file *ast.File, typeExpr ast.Expr) (*spec.RefOrSpec[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, *ast.IndexListExpr: + name, err := getExtendedGenericFieldType(file, expr, nil) + if err == nil { + if schema, err := p.getTypeSchemaV3(name, file, false); err == nil { + return schema, nil + } + } + + p.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead. (%s)\n", typeExpr, err) + default: + p.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) + } + + return PrimitiveSchemaV3(OBJECT), nil +} diff --git a/go.mod b/go.mod index 515649377..4bbb55185 100644 --- a/go.mod +++ b/go.mod @@ -5,28 +5,32 @@ go 1.18 require ( github.com/KyleBanks/depth v1.2.1 github.com/ghodss/yaml v1.0.0 - github.com/go-openapi/spec v0.20.4 - github.com/stretchr/testify v1.7.0 - github.com/urfave/cli/v2 v2.3.0 - golang.org/x/tools v0.1.12 + github.com/go-openapi/spec v0.20.8 + github.com/json-iterator/go v1.1.12 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.8.2 + github.com/sv-tools/openapi v0.2.1 + golang.org/x/tools v0.7.0 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect ) require ( - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.6 // indirect - github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/mailru/easyjson v0.7.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/russross/blackfriday/v2 v2.0.1 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect + github.com/urfave/cli/v2 v2.25.1 + golang.org/x/sys v0.7.0 // indirect + gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 939ae2086..177001055 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,7 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -14,65 +9,77 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU= +github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA= +github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg= +github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= +github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/operation.go b/operation.go index 61b1f9c32..c69b151e3 100644 --- a/operation.go +++ b/operation.go @@ -6,6 +6,7 @@ import ( "go/ast" goparser "go/parser" "go/token" + "log" "net/http" "os" "path/filepath" @@ -15,6 +16,7 @@ import ( "github.com/go-openapi/spec" "golang.org/x/tools/go/loader" + "gopkg.in/yaml.v2" ) // RouteProperties describes HTTP properties of a single router comment. @@ -160,17 +162,28 @@ func (operation *Operation) ParseComment(comment string, astFile *ast.File) erro // ParseCodeSample godoc. func (operation *Operation) ParseCodeSample(attribute, _, lineRemainder string) error { + log.Println("line remainder:", lineRemainder) + if lineRemainder == "file" { - data, err := getCodeExampleForSummary(operation.Summary, operation.codeExampleFilesDir) + log.Println("line remainder is file") + + data, isJSON, err := getCodeExampleForSummary(operation.Summary, operation.codeExampleFilesDir) if err != nil { return err } var valueJSON interface{} - err = json.Unmarshal(data, &valueJSON) - if err != nil { - return fmt.Errorf("annotation %s need a valid json value", attribute) + if isJSON { + err = json.Unmarshal(data, &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) + } + } else { + err = yaml.Unmarshal(data, &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid yaml value. error: %s", attribute, err.Error()) + } } // don't use the method provided by spec lib, because it will call toLower() on attribute names, which is wrongly @@ -206,7 +219,7 @@ func (operation *Operation) ParseMetadata(attribute, lowerAttribute, lineRemaind err := json.Unmarshal([]byte(lineRemainder), &valueJSON) if err != nil { - return fmt.Errorf("annotation %s need a valid json value", attribute) + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) } // don't use the method provided by spec lib, because it will call toLower() on attribute names, which is wrongly @@ -1176,10 +1189,10 @@ func createParameter(paramType, description, paramName, objectType, schemaType s return result } -func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error) { +func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, bool, error) { dirEntries, err := os.ReadDir(dirPath) if err != nil { - return nil, err + return nil, false, err } for _, entry := range dirEntries { @@ -1189,7 +1202,9 @@ func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error fileName := entry.Name() - if !strings.Contains(fileName, ".json") { + isJson := strings.Contains(fileName, ".json") + isYaml := strings.Contains(fileName, ".yaml") + if !isJson && !isYaml { continue } @@ -1198,12 +1213,12 @@ func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error commentInfo, err := os.ReadFile(fullPath) if err != nil { - return nil, fmt.Errorf("Failed to read code example file %s error: %s ", fullPath, err) + return nil, false, fmt.Errorf("Failed to read code example file %s error: %s ", fullPath, err) } - return commentInfo, nil + return commentInfo, isJson, nil } } - return nil, fmt.Errorf("unable to find code example file for tag %s in the given directory", summaryName) + return nil, false, fmt.Errorf("unable to find code example file for tag %s in the given directory", summaryName) } diff --git a/operation_test.go b/operation_test.go index e71bda4ee..87bcae9ce 100644 --- a/operation_test.go +++ b/operation_test.go @@ -9,6 +9,7 @@ import ( "github.com/go-openapi/spec" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseEmptyComment(t *testing.T) { @@ -2202,7 +2203,7 @@ func TestParseExtentions(t *testing.T) { operation := NewOperation(nil) err := operation.ParseComment(comment, nil) - assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a valid json value") + assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a valid json value. error: invalid character '}' after array element") } // OK @@ -2345,10 +2346,12 @@ func TestParseCodeSamples(t *testing.T) { operation.Summary = "example" err := operation.ParseComment(comment, nil) - assert.NoError(t, err, "no error should be thrown") - assert.Equal(t, operation.Summary, "example") - assert.Equal(t, operation.Extensions["x-codeSamples"], - map[string]interface{}{"lang": "JavaScript", "source": "console.log('Hello World');"}) + require.NoError(t, err, "no error should be thrown") + + assert.Equal(t, "example", operation.Summary) + assert.Equal(t, []interface{}([]interface{}{map[string]interface{}{"lang": "JavaScript", "source": "console.log('Hello World');"}}), + operation.Extensions["x-codeSamples"], + ) }) t.Run("With broken file sample", func(t *testing.T) { diff --git a/operationv3.go b/operationv3.go new file mode 100644 index 000000000..219171634 --- /dev/null +++ b/operationv3.go @@ -0,0 +1,993 @@ +package swag + +import ( + "encoding/json" + "fmt" + "go/ast" + "log" + "net/http" + "strconv" + "strings" + + "github.com/sv-tools/openapi/spec" + "gopkg.in/yaml.v2" +) + +// Operation describes a single API operation on a path. +// For more information: https://github.com/swaggo/swag#api-operation +type OperationV3 struct { + parser *Parser + codeExampleFilesDir string + spec.Operation + RouterProperties []RouteProperties +} + +// NewOperationV3 returns a new instance of OperationV3. +func NewOperationV3(parser *Parser, options ...func(*OperationV3)) *OperationV3 { + op := *spec.NewOperation().Spec + op.Responses = spec.NewResponses() + + operation := &OperationV3{ + parser: parser, + Operation: op, + } + + for _, option := range options { + option(operation) + } + + return operation +} + +// SetCodeExampleFilesDirectory sets the directory to search for codeExamples. +func SetCodeExampleFilesDirectoryV3(directoryPath string) func(*OperationV3) { + return func(o *OperationV3) { + o.codeExampleFilesDir = directoryPath + } +} + +// ParseComment parses comment for given comment string and returns error if error occurs. +func (o *OperationV3) ParseComment(comment string, astFile *ast.File) error { + commentLine := strings.TrimSpace(strings.TrimLeft(comment, "/")) + if len(commentLine) == 0 { + return nil + } + + fields := FieldsByAnySpace(commentLine, 2) + attribute := fields[0] + lowerAttribute := strings.ToLower(attribute) + var lineRemainder string + if len(fields) > 1 { + lineRemainder = fields[1] + } + switch lowerAttribute { + case descriptionAttr: + o.ParseDescriptionComment(lineRemainder) + case descriptionMarkdownAttr: + commentInfo, err := getMarkdownForTag(lineRemainder, o.parser.markdownFileDir) + if err != nil { + return err + } + + o.ParseDescriptionComment(string(commentInfo)) + case summaryAttr: + o.Summary = lineRemainder + case idAttr: + o.OperationID = lineRemainder + case tagsAttr: + o.ParseTagsComment(lineRemainder) + case acceptAttr: + return o.ParseAcceptComment(lineRemainder) + case produceAttr: + return o.ParseProduceComment(lineRemainder) + case paramAttr: + return o.ParseParamComment(lineRemainder, astFile) + case successAttr, failureAttr, responseAttr: + return o.ParseResponseComment(lineRemainder, astFile) + case headerAttr: + return o.ParseResponseHeaderComment(lineRemainder, astFile) + case routerAttr: + return o.ParseRouterComment(lineRemainder) + case securityAttr: + return o.ParseSecurityComment(lineRemainder) + case deprecatedAttr: + o.Deprecated = true + case xCodeSamplesAttr, xCodeSamplesAttrOriginal: + return o.ParseCodeSample(attribute, commentLine, lineRemainder) + default: + return o.ParseMetadata(attribute, lowerAttribute, lineRemainder) + } + + return nil +} + +// ParseDescriptionComment parses the description comment and sets it to the operation. +func (o *OperationV3) ParseDescriptionComment(lineRemainder string) { + if o.Description == "" { + o.Description = lineRemainder + + return + } + + o.Description += "\n" + lineRemainder +} + +// ParseMetadata godoc. +func (o *OperationV3) ParseMetadata(attribute, lowerAttribute, lineRemainder string) error { + // parsing specific meta data extensions + if strings.HasPrefix(lowerAttribute, "@x-") { + if len(lineRemainder) == 0 { + return fmt.Errorf("annotation %s need a value", attribute) + } + + var valueJSON any + + err := json.Unmarshal([]byte(lineRemainder), &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) + } + + o.Responses.Extensions[attribute[1:]] = valueJSON + return nil + } + + return nil +} + +// ParseTagsComment parses comment for given `tag` comment string. +func (o *OperationV3) ParseTagsComment(commentLine string) { + for _, tag := range strings.Split(commentLine, ",") { + o.Tags = append(o.Tags, strings.TrimSpace(tag)) + } +} + +// ParseAcceptComment parses comment for given `accept` comment string. +func (o *OperationV3) ParseAcceptComment(commentLine string) error { + const errMessage = "could not parse accept comment" + + // TODO this must be moved into another comment + // return parseMimeTypeList(commentLine, &o.RequestBody.Spec.Spec.Content, ) + // result, err := parseMimeTypeListV3(commentLine, "%v accept type can't be accepted") + // if err != nil { + // return errors.Wrap(err, errMessage) + // } + + // for _, value := range result { + // o.RequestBody.Spec.Spec.Content[value] = spec.NewMediaType() + // } + + return nil +} + +// ParseProduceComment parses comment for given `produce` comment string. +func (o *OperationV3) ParseProduceComment(commentLine string) error { + const errMessage = "could not parse produce comment" + // return parseMimeTypeList(commentLine, &o.Responses, "%v produce type can't be accepted") + + // result, err := parseMimeTypeListV3(commentLine, "%v accept type can't be accepted") + // if err != nil { + // return errors.Wrap(err, errMessage) + // } + + // for _, value := range result { + // o.Responses.Spec.Response + // } + + // TODO the format of the comment needs to be changed in order to work + // The produce can be different per response code, so the produce mimetype needs to be included in the response comment + + return nil +} + +// parseMimeTypeList parses a list of MIME Types for a comment like +// `produce` (`Content-Type:` response header) or +// `accept` (`Accept:` request header). +func parseMimeTypeListV3(mimeTypeList string, format string) ([]string, error) { + var result []string + for _, typeName := range strings.Split(mimeTypeList, ",") { + if mimeTypePattern.MatchString(typeName) { + result = append(result, typeName) + + continue + } + + aliasMimeType, ok := mimeTypeAliases[typeName] + if !ok { + return nil, fmt.Errorf(format, typeName) + } + + result = append(result, aliasMimeType) + } + + return result, nil +} + +// ParseParamComment parses params return []string of param properties +// E.g. @Param queryText formData string true "The email for login" +// +// [param name] [paramType] [data type] [is mandatory?] [Comment] +// +// E.g. @Param some_id path int true "Some ID". +func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) error { + matches := paramPattern.FindStringSubmatch(commentLine) + if len(matches) != 6 { + return fmt.Errorf("missing required param comment parameters \"%s\"", commentLine) + } + + name := matches[1] + paramType := matches[2] + refType := TransToValidSchemeType(matches[3]) + + // Detect refType + objectType := OBJECT + + if strings.HasPrefix(refType, "[]") { + objectType = ARRAY + refType = strings.TrimPrefix(refType, "[]") + refType = TransToValidSchemeType(refType) + } else if IsPrimitiveType(refType) || + paramType == "formData" && refType == "file" { + objectType = PRIMITIVE + } + + var enums []interface{} + if !IsPrimitiveType(refType) { + schema, _ := o.parser.getTypeSchemaV3(refType, astFile, false) + if schema != nil && schema.Spec != nil && schema.Spec.Enum != nil { + // schema.Spec.Type != ARRAY + fmt.Println(schema.Spec.Type) + + if objectType == OBJECT { + objectType = PRIMITIVE + } + refType = TransToValidSchemeType(schema.Spec.Type[0]) + enums = schema.Spec.Enum + } + } + + requiredText := strings.ToLower(matches[4]) + required := requiredText == "true" || requiredText == requiredLabel + description := matches[5] + + param := createParameterV3(paramType, description, name, objectType, refType, required, enums, o.parser.collectionFormatInQuery) + + switch paramType { + case "path", "header": + switch objectType { + case ARRAY: + if !IsPrimitiveType(refType) { + return fmt.Errorf("%s is not supported array type for %s", refType, paramType) + } + case OBJECT: + return fmt.Errorf("%s is not supported type for %s", refType, paramType) + } + case "query", "formData": + switch objectType { + case ARRAY: + if !IsPrimitiveType(refType) && !(refType == "file" && paramType == "formData") { + return fmt.Errorf("%s is not supported array type for %s", refType, paramType) + } + case PRIMITIVE: + break + case OBJECT: + schema, err := o.parser.getTypeSchemaV3(refType, astFile, false) + if err != nil { + return err + } + + if len(schema.Spec.Properties) == 0 { + return nil + } + + for name, item := range schema.Spec.Properties { + prop := item.Spec + if len(prop.Type) == 0 { + continue + } + + switch { + case prop.Type[0] == ARRAY && + prop.Items.Schema != nil && + len(prop.Items.Schema.Spec.Type) > 0 && + IsSimplePrimitiveType(prop.Items.Schema.Spec.Type[0]): + + param = createParameterV3(paramType, prop.Description, name, prop.Type[0], prop.Items.Schema.Spec.Type[0], findInSlice(schema.Spec.Required, name), enums, o.parser.collectionFormatInQuery) + + case IsSimplePrimitiveType(prop.Type[0]): + param = createParameterV3(paramType, prop.Description, name, PRIMITIVE, prop.Type[0], findInSlice(schema.Spec.Required, name), enums, o.parser.collectionFormatInQuery) + default: + o.parser.debug.Printf("skip field [%s] in %s is not supported type for %s", name, refType, paramType) + + continue + } + + param.Schema.Spec = prop + + listItem := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ + Spec: &spec.Extendable[spec.Parameter]{ + Spec: ¶m, + }, + } + + o.Operation.Parameters = append(o.Operation.Parameters, listItem) + } + + return nil + } + case "body": + if objectType == PRIMITIVE { + param.Schema = PrimitiveSchemaV3(refType) + } else { + schema, err := o.parseAPIObjectSchema(commentLine, objectType, refType, astFile) + if err != nil { + return err + } + + param.Schema = schema + } + default: + return fmt.Errorf("%s is not supported paramType", paramType) + } + + err := o.parseParamAttribute(commentLine, objectType, refType, ¶m) + if err != nil { + return err + } + + item := spec.NewRefOrSpec(nil, &spec.Extendable[spec.Parameter]{ + Spec: ¶m, + }) + + o.Operation.Parameters = append(o.Operation.Parameters, item) + + return nil +} + +func (o *OperationV3) parseParamAttribute(comment, objectType, schemaType string, param *spec.Parameter) error { + schemaType = TransToValidSchemeType(schemaType) + + for attrKey, re := range regexAttributes { + attr, err := findAttr(re, comment) + if err != nil { + continue + } + + switch attrKey { + case enumsTag: + err = setEnumParamV3(param, attr, objectType, schemaType) + case minimumTag, maximumTag: + err = setNumberParamV3(param, attrKey, schemaType, attr, comment) + case defaultTag: + err = setDefaultV3(param, schemaType, attr) + case minLengthTag, maxLengthTag: + err = setStringParamV3(param, attrKey, schemaType, attr, comment) + case formatTag: + param.Schema.Spec.Format = attr + case exampleTag: + err = setExampleV3(param, schemaType, attr) + case schemaExampleTag: + err = setSchemaExampleV3(param, schemaType, attr) + case extensionsTag: + param.Schema.Spec.Extensions = setExtensionParam(attr) + case collectionFormatTag: + err = setCollectionFormatParamV3(param, attrKey, objectType, attr, comment) + } + + if err != nil { + return err + } + } + + return nil +} + +func setCollectionFormatParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { + if schemaType == ARRAY { + param.Style = TransToValidCollectionFormatV3(attr, param.In) + return nil + } + + return fmt.Errorf("%s is attribute to set to an array. comment=%s got=%s", name, commentLine, schemaType) +} + +func setSchemaExampleV3(param *spec.Parameter, schemaType string, value string) error { + val, err := defineType(schemaType, value) + if err != nil { + return nil // Don't set a example value if it's not valid + } + // skip schema + if param.Schema == nil { + return nil + } + + switch v := val.(type) { + case string: + // replaces \r \n \t in example string values. + param.Schema.Spec.Example = strings.NewReplacer(`\r`, "\r", `\n`, "\n", `\t`, "\t").Replace(v) + default: + param.Schema.Spec.Example = val + } + + return nil +} + +func setExampleV3(param *spec.Parameter, schemaType string, value string) error { + val, err := defineType(schemaType, value) + if err != nil { + return nil // Don't set a example value if it's not valid + } + + param.Example = val + + return nil +} + +func setStringParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { + if schemaType != STRING { + return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) + } + + n, err := strconv.Atoi(attr) + if err != nil { + return fmt.Errorf("%s is allow only a number got=%s", name, attr) + } + + switch name { + case minLengthTag: + param.Schema.Spec.MinLength = &n + case maxLengthTag: + param.Schema.Spec.MaxLength = &n + } + + return nil +} + +func setDefaultV3(param *spec.Parameter, schemaType string, value string) error { + val, err := defineType(schemaType, value) + if err != nil { + return nil // Don't set a default value if it's not valid + } + + param.Schema.Spec.Default = val + + return nil +} + +func setEnumParamV3(param *spec.Parameter, attr, objectType, schemaType string) error { + for _, e := range strings.Split(attr, ",") { + e = strings.TrimSpace(e) + + value, err := defineType(schemaType, e) + if err != nil { + return err + } + + switch objectType { + case ARRAY: + param.Schema.Spec.Items.Schema.Spec.Enum = append(param.Schema.Spec.Items.Schema.Spec.Enum, value) + default: + param.Schema.Spec.Enum = append(param.Schema.Spec.Enum, value) + } + } + + return nil +} + +func setNumberParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { + switch schemaType { + case INTEGER, NUMBER: + n, err := strconv.Atoi(attr) + if err != nil { + return fmt.Errorf("maximum is allow only a number. comment=%s got=%s", commentLine, attr) + } + + switch name { + case minimumTag: + param.Schema.Spec.Minimum = &n + case maximumTag: + param.Schema.Spec.Maximum = &n + } + + return nil + default: + return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) + } +} + +func (o *OperationV3) parseAPIObjectSchema(commentLine, schemaType, refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { + if strings.HasSuffix(refType, ",") && strings.Contains(refType, "[") { + // regexp may have broken generic syntax. find closing bracket and add it back + allMatchesLenOffset := strings.Index(commentLine, refType) + len(refType) + lostPartEndIdx := strings.Index(commentLine[allMatchesLenOffset:], "]") + if lostPartEndIdx >= 0 { + refType += commentLine[allMatchesLenOffset : allMatchesLenOffset+lostPartEndIdx+1] + } + } + + switch schemaType { + case OBJECT: + if !strings.HasPrefix(refType, "[]") { + return o.parseObjectSchema(refType, astFile) + } + + refType = refType[2:] + + fallthrough + case ARRAY: + schema, err := o.parseObjectSchema(refType, astFile) + if err != nil { + return nil, err + } + + result := spec.NewSchemaSpec() + result.Spec.Type = spec.NewSingleOrArray("array") + result.Spec.Items = spec.NewBoolOrSchema(false, schema) //TODO: allowed? + return result, nil + + default: + return PrimitiveSchemaV3(schemaType), nil + } +} + +// ParseRouterComment parses comment for given `router` comment string. +func (o *OperationV3) ParseRouterComment(commentLine string) error { + matches := routerPattern.FindStringSubmatch(commentLine) + if len(matches) != 3 { + return fmt.Errorf("can not parse router comment \"%s\"", commentLine) + } + + signature := RouteProperties{ + Path: matches[1], + HTTPMethod: strings.ToUpper(matches[2]), + } + + if _, ok := allMethod[signature.HTTPMethod]; !ok { + return fmt.Errorf("invalid method: %s", signature.HTTPMethod) + } + + o.RouterProperties = append(o.RouterProperties, signature) + + return nil +} + +// createParameter returns swagger spec.Parameter for given paramType, description, paramName, schemaType, required. +func createParameterV3(in, description, paramName, objectType, schemaType string, required bool, enums []interface{}, collectionFormat string) spec.Parameter { + // //five possible parameter types. query, path, body, header, form + result := spec.Parameter{ + Description: description, + Required: required, + Name: paramName, + In: in, + Schema: spec.NewRefOrSpec(nil, &spec.Schema{}), + } + + if in == "body" { + return result + } + + switch objectType { + case ARRAY: + result.Schema.Spec.Type = spec.NewSingleOrArray(objectType) + result.Schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + result.Schema.Spec.Items.Schema.Spec.Type = spec.NewSingleOrArray(schemaType) + result.Schema.Spec.Enum = enums + case PRIMITIVE, OBJECT: + result.Schema.Spec.Type = spec.NewSingleOrArray(schemaType) + result.Schema.Spec.Enum = enums + } + + return result +} + +func (o *OperationV3) parseObjectSchema(refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { + return parseObjectSchemaV3(o.parser, refType, astFile) +} + +func parseObjectSchemaV3(parser *Parser, refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { + switch { + case refType == NIL: + return nil, nil + case refType == INTERFACE: + return PrimitiveSchemaV3(OBJECT), nil + case refType == ANY: + return PrimitiveSchemaV3(OBJECT), nil + case IsGolangPrimitiveType(refType): + refType = TransToValidSchemeType(refType) + + return PrimitiveSchemaV3(refType), nil + case IsPrimitiveType(refType): + return PrimitiveSchemaV3(refType), nil + case strings.HasPrefix(refType, "[]"): + schema, err := parseObjectSchemaV3(parser, refType[2:], astFile) + if err != nil { + return nil, err + } + + result := spec.NewSchemaSpec() + result.Spec.Type = spec.NewSingleOrArray("array") + result.Spec.Items = spec.NewBoolOrSchema(false, schema) + + return result, nil + case strings.HasPrefix(refType, "map["): + // ignore key type + idx := strings.Index(refType, "]") + if idx < 0 { + return nil, fmt.Errorf("invalid type: %s", refType) + } + + refType = refType[idx+1:] + if refType == INTERFACE || refType == ANY { + schema := &spec.Schema{} + schema.AdditionalProperties = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + schema.Type = spec.NewSingleOrArray(OBJECT) + refOrSpec := spec.NewRefOrSpec(nil, schema) + return refOrSpec, nil + } + + schema, err := parseObjectSchemaV3(parser, refType, astFile) + if err != nil { + return nil, err + } + + result := &spec.Schema{} + result.AdditionalProperties = spec.NewBoolOrSchema(false, schema) + result.Type = spec.NewSingleOrArray(OBJECT) + refOrSpec := spec.NewSchemaSpec() + refOrSpec.Spec = result + + return refOrSpec, nil + case strings.Contains(refType, "{"): + return parseCombinedObjectSchemaV3(parser, refType, astFile) + default: + if parser != nil { // checking refType has existing in 'TypeDefinitions' + schema, err := parser.getTypeSchemaV3(refType, astFile, true) + if err != nil { + return nil, err + } + + return schema, nil + } + + return spec.NewSchemaRef(spec.NewRef("#/components/schemas/" + refType)), nil + } +} + +// ParseResponseHeaderComment parses comment for given `response header` comment string. +func (o *OperationV3) ParseResponseHeaderComment(commentLine string, _ *ast.File) error { + matches := responsePattern.FindStringSubmatch(commentLine) + if len(matches) != 5 { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + header := newHeaderSpecV3(strings.Trim(matches[2], "{}"), strings.Trim(matches[4], "\"")) + + headerKey := strings.TrimSpace(matches[3]) + + if strings.EqualFold(matches[1], "all") { + if o.Responses.Spec.Default != nil { + o.Responses.Spec.Default.Spec.Spec.Headers[headerKey] = header + } + + if o.Responses.Spec.Response != nil { + for _, v := range o.Responses.Spec.Response { + v.Spec.Spec.Headers[headerKey] = header + + } + } + + return nil + } + + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, defaultTag) { + if o.Responses.Spec.Default != nil { + o.Responses.Spec.Default.Spec.Spec.Headers[headerKey] = header + } + + continue + } + + _, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + // TODO check condition + if o.Responses != nil && o.Responses.Spec != nil && o.Responses.Spec.Response != nil { + response, responseExist := o.Responses.Spec.Response[codeStr] + if responseExist { + response.Spec.Spec.Headers[headerKey] = header + o.Responses.Spec.Response[codeStr] = response + } + } + } + + return nil +} + +func newHeaderSpecV3(schemaType, description string) *spec.RefOrSpec[spec.Extendable[spec.Header]] { + result := spec.NewHeaderSpec() + result.Spec.Spec.Description = description + result.Spec.Spec.Schema = spec.NewSchemaSpec() + result.Spec.Spec.Schema.Spec.Type = spec.NewSingleOrArray(schemaType) + + return result +} + +// ParseResponseComment parses comment for given `response` comment string. +func (o *OperationV3) ParseResponseComment(commentLine string, astFile *ast.File) error { + matches := responsePattern.FindStringSubmatch(commentLine) + if len(matches) != 5 { + err := o.ParseEmptyResponseComment(commentLine) + if err != nil { + return o.ParseEmptyResponseOnly(commentLine) + } + + return err + } + + description := strings.Trim(matches[4], "\"") + + schema, err := o.parseAPIObjectSchema(commentLine, strings.Trim(matches[2], "{}"), strings.TrimSpace(matches[3]), astFile) + if err != nil { + return err + } + + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, defaultTag) { + response := o.DefaultResponse() + response.Description = description + + mimeType := "application/json" // TODO: set correct mimeType + setResponseSchema(response, mimeType, schema) + + continue + } + + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + if description == "" { + description = http.StatusText(code) + } + + response := spec.NewResponseSpec() + response.Spec.Spec.Description = description + + mimeType := "application/json" // TODO: set correct mimeType + setResponseSchema(response.Spec.Spec, mimeType, schema) + + o.AddResponse(codeStr, response) + } + + return nil +} + +// setResponseSchema sets response schema for given response. +func setResponseSchema(response *spec.Response, mimeType string, schema *spec.RefOrSpec[spec.Schema]) { + mediaType := spec.NewMediaType() + mediaType.Spec.Schema = schema + + if response.Content == nil { + response.Content = make(map[string]*spec.Extendable[spec.MediaType]) + } + + response.Content[mimeType] = mediaType +} + +// ParseEmptyResponseComment parse only comment out status code and description,eg: @Success 200 "it's ok". +func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error { + matches := emptyResponsePattern.FindStringSubmatch(commentLine) + if len(matches) != 3 { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + description := strings.Trim(matches[2], "\"") + + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, defaultTag) { + response := o.DefaultResponse() + response.Description = description + + continue + } + + _, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + o.AddResponse(codeStr, newResponseWithDescription(description)) + } + + return nil +} + +// DefaultResponse return the default response member pointer. +func (o *OperationV3) DefaultResponse() *spec.Response { + if o.Responses.Spec.Default == nil { + o.Responses.Spec.Default = spec.NewResponseSpec() + o.Responses.Spec.Default.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) + } + + if o.Responses.Spec.Default.Spec.Spec.Content == nil { + o.Responses.Spec.Default.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) + } + + return o.Responses.Spec.Default.Spec.Spec +} + +// AddResponse add a response for a code. +func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Extendable[spec.Response]]) { + if response.Spec.Spec.Headers == nil { + response.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) + } + + if o.Responses.Spec.Response == nil { + o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]]) + } + + o.Responses.Spec.Response[code] = response +} + +// ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200. +func (o *OperationV3) ParseEmptyResponseOnly(commentLine string) error { + for _, codeStr := range strings.Split(commentLine, ",") { + if strings.EqualFold(codeStr, defaultTag) { + _ = o.DefaultResponse() + + continue + } + + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + o.AddResponse(codeStr, newResponseWithDescription(http.StatusText(code))) + } + + return nil +} + +func newResponseWithDescription(description string) *spec.RefOrSpec[spec.Extendable[spec.Response]] { + response := spec.NewResponseSpec() + response.Spec.Spec.Description = description + return response +} + +func parseCombinedObjectSchemaV3(parser *Parser, refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { + matches := combinedPattern.FindStringSubmatch(refType) + if len(matches) != 3 { + return nil, fmt.Errorf("invalid type: %s", refType) + } + + schema, err := parseObjectSchemaV3(parser, matches[1], astFile) + if err != nil { + return nil, err + } + + fields, props := parseFields(matches[2]), map[string]*spec.RefOrSpec[spec.Schema]{} + + for _, field := range fields { + keyVal := strings.SplitN(field, "=", 2) + if len(keyVal) != 2 { + continue + } + + schema, err := parseObjectSchemaV3(parser, keyVal[1], astFile) + if err != nil { + return nil, err + } + + props[keyVal[0]] = schema + } + + if len(props) == 0 { + return schema, nil + } + + if schema.Ref == nil && + len(schema.Spec.Type) > 0 && + schema.Spec.Type[0] == OBJECT && + len(schema.Spec.Properties) == 0 && + schema.Spec.AdditionalProperties == nil { + schema.Spec.Properties = props + return schema, nil + } + + schemaRefPath := strings.Replace(schema.Ref.Ref, "#/components/schemas/", "", 1) + schemaSpec := parser.openAPI.Components.Spec.Schemas[schemaRefPath] + schemaSpec.Spec.JsonSchemaComposition.AllOf = make([]*spec.RefOrSpec[spec.Schema], len(props)) + + i := 0 + for name, prop := range props { + wrapperSpec := spec.NewSchemaSpec() + wrapperSpec.Spec = &spec.Schema{} + wrapperSpec.Spec.Type = spec.NewSingleOrArray(OBJECT) + wrapperSpec.Spec.Properties = map[string]*spec.RefOrSpec[spec.Schema]{ + name: prop, + } + + parser.openAPI.Components.Spec.Schemas[name] = wrapperSpec + + ref := spec.NewRefOrSpec[spec.Schema](spec.NewRef("#/components/schemas/"+name), nil) + + schemaSpec.Spec.JsonSchemaComposition.AllOf[i] = ref + i++ + } + + return schemaSpec, nil +} + +// ParseSecurityComment parses comment for given `security` comment string. +func (o *OperationV3) ParseSecurityComment(commentLine string) error { + var ( + securityMap = make(map[string][]string) + securitySource = commentLine[strings.Index(commentLine, "@Security")+1:] + ) + + for _, securityOption := range strings.Split(securitySource, "||") { + securityOption = strings.TrimSpace(securityOption) + + left, right := strings.Index(securityOption, "["), strings.Index(securityOption, "]") + + if !(left == -1 && right == -1) { + scopes := securityOption[left+1 : right] + + var options []string + + for _, scope := range strings.Split(scopes, ",") { + options = append(options, strings.TrimSpace(scope)) + } + + securityKey := securityOption[0:left] + securityMap[securityKey] = append(securityMap[securityKey], options...) + } else { + securityKey := strings.TrimSpace(securityOption) + securityMap[securityKey] = []string{} + } + } + + o.Security = append(o.Security, securityMap) + + return nil +} + +// ParseCodeSample godoc. +func (o *OperationV3) ParseCodeSample(attribute, _, lineRemainder string) error { + log.Println("line remainder:", lineRemainder) + + if lineRemainder == "file" { + log.Println("line remainder is file") + + data, isJSON, err := getCodeExampleForSummary(o.Summary, o.codeExampleFilesDir) + if err != nil { + return err + } + + // using custom type, as json marshaller has problems with []map[interface{}]map[interface{}]interface{} + var valueJSON CodeSamples + + if isJSON { + err = json.Unmarshal(data, &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) + } + } else { + err = yaml.Unmarshal(data, &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid yaml value. error: %s", attribute, err.Error()) + } + } + + o.Responses.Extensions[attribute[1:]] = valueJSON + + return nil + } + + // Fallback into existing logic + return o.ParseMetadata(attribute, strings.ToLower(attribute), lineRemainder) +} diff --git a/operationv3_test.go b/operationv3_test.go new file mode 100644 index 000000000..5b552caca --- /dev/null +++ b/operationv3_test.go @@ -0,0 +1,1916 @@ +package swag + +import ( + goparser "go/parser" + "go/token" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/sv-tools/openapi/spec" +) + +var typeObject = spec.SingleOrArray[string](spec.SingleOrArray[string]{OBJECT}) +var typeArray = spec.SingleOrArray[string](spec.SingleOrArray[string]{ARRAY}) +var typeInteger = spec.SingleOrArray[string](spec.SingleOrArray[string]{INTEGER}) +var typeString = spec.SingleOrArray[string](spec.SingleOrArray[string]{STRING}) +var typeFile = spec.SingleOrArray[string](spec.SingleOrArray[string]{"file"}) +var typeNumber = spec.SingleOrArray[string](spec.SingleOrArray[string]{NUMBER}) +var typeBool = spec.SingleOrArray[string](spec.SingleOrArray[string]{BOOLEAN}) + +func TestParseEmptyCommentV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + err := operation.ParseComment("//", nil) + + require.NoError(t, err) +} + +func TestParseTagsCommentV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + err := operation.ParseComment(`/@Tags pet, store,user`, nil) + require.NoError(t, err) + assert.Equal(t, operation.Tags, []string{"pet", "store", "user"}) +} + +func TestParseRouterCommentV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{wishlist_id} [get]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + assert.Len(t, operation.RouterProperties, 1) + assert.Equal(t, "/customer/get-wishlist/{wishlist_id}", operation.RouterProperties[0].Path) + assert.Equal(t, "GET", operation.RouterProperties[0].HTTPMethod) +} + +func TestParseRouterMultipleCommentsV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{wishlist_id} [get]` + anotherComment := `/@Router /customer/get-the-wishlist/{wishlist_id} [post]` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + err = operation.ParseComment(anotherComment, nil) + require.NoError(t, err) + + assert.Len(t, operation.RouterProperties, 2) + assert.Equal(t, "/customer/get-wishlist/{wishlist_id}", operation.RouterProperties[0].Path) + assert.Equal(t, "GET", operation.RouterProperties[0].HTTPMethod) + assert.Equal(t, "/customer/get-the-wishlist/{wishlist_id}", operation.RouterProperties[1].Path) + assert.Equal(t, "POST", operation.RouterProperties[1].HTTPMethod) +} + +func TestParseRouterOnlySlashV3(t *testing.T) { + t.Parallel() + + comment := `// @Router / [get]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + assert.Len(t, operation.RouterProperties, 1) + assert.Equal(t, "/", operation.RouterProperties[0].Path) + assert.Equal(t, "GET", operation.RouterProperties[0].HTTPMethod) +} + +func TestParseRouterCommentWithPlusSignV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{proxy+} [post]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + assert.Len(t, operation.RouterProperties, 1) + assert.Equal(t, "/customer/get-wishlist/{proxy+}", operation.RouterProperties[0].Path) + assert.Equal(t, "POST", operation.RouterProperties[0].HTTPMethod) +} + +func TestParseRouterCommentWithDollarSignV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{wishlist_id}$move [post]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + assert.Len(t, operation.RouterProperties, 1) + assert.Equal(t, "/customer/get-wishlist/{wishlist_id}$move", operation.RouterProperties[0].Path) + assert.Equal(t, "POST", operation.RouterProperties[0].HTTPMethod) +} + +func TestParseRouterCommentNoDollarSignAtPathStartErrV3(t *testing.T) { + t.Parallel() + + comment := `/@Router $customer/get-wishlist/{wishlist_id}$move [post]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseRouterCommentWithColonSignV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{wishlist_id}:move [post]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + assert.Len(t, operation.RouterProperties, 1) + assert.Equal(t, "/customer/get-wishlist/{wishlist_id}:move", operation.RouterProperties[0].Path) + assert.Equal(t, "POST", operation.RouterProperties[0].HTTPMethod) +} + +func TestParseRouterCommentNoColonSignAtPathStartErrV3(t *testing.T) { + t.Parallel() + + comment := `/@Router :customer/get-wishlist/{wishlist_id}:move [post]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseRouterCommentMethodSeparationErrV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /api/{id}|,*[get` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseRouterCommentMethodMissingErrV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{wishlist_id}` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestOperation_ParseResponseWithDefaultV3(t *testing.T) { + t.Parallel() + + comment := `@Success default {object} nil "An empty response"` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + assert.Equal(t, "An empty response", operation.Responses.Spec.Default.Spec.Spec.Description) + + comment = `@Success 200,default {string} Response "A response"` + operation = NewOperationV3(nil) + + err = operation.ParseComment(comment, nil) + require.NoError(t, err) + + assert.Equal(t, "A response", operation.Responses.Spec.Default.Spec.Spec.Description) + assert.Equal(t, "A response", operation.Responses.Spec.Response["200"].Spec.Spec.Description) +} + +func TestParseResponseSuccessCommentWithEmptyResponseV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} nil "An empty response"` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `An empty response`, response.Spec.Spec.Description) +} + +func TestParseResponseFailureCommentWithEmptyResponseV3(t *testing.T) { + t.Parallel() + + comment := `@Failure 500 {object} nil` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + assert.Equal(t, "Internal Server Error", operation.Responses.Spec.Response["500"].Spec.Spec.Description) +} + +func TestParseResponseCommentWithObjectTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.OrderRow "Error message, if code != 200` + parser := New() + operation := NewOperationV3(parser) + operation.parser.addTestType("model.OrderRow") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + + assert.Equal(t, "#/components/schemas/model.OrderRow", response.Spec.Spec.Content["application/json"].Spec.Schema.Ref.Ref) +} + +func TestParseResponseCommentWithNestedPrimitiveTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data=string,data2=int} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + require.NotNil(t, response.Spec.Spec.Content["application/json"].Spec.Schema) + + allOf := operation.Responses.Spec.Response["200"].Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf + require.NotNil(t, allOf) + assert.Equal(t, 2, len(allOf)) + assert.Equal(t, "#/components/schemas/data", allOf[0].Ref.Ref) + assert.Equal(t, "#/components/schemas/data2", allOf[1].Ref.Ref) +} + +func TestParseResponseCommentWithNestedPrimitiveArrayTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data=[]string,data2=[]int} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + assert.NotNil(t, operation.parser.openAPI.Components.Spec.Schemas["data"].Spec.Properties["data"]) + assert.Equal(t, spec.SingleOrArray[string](spec.SingleOrArray[string]{"string"}), operation.parser.openAPI.Components.Spec.Schemas["data"].Spec.Properties["data"].Spec.Items.Schema.Spec.Type) +} + +func TestParseResponseCommentWithNestedObjectTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data=model.Payload,data2=model.Payload2} "Error message, if code != 200` + operation := NewOperationV3(New()) + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.Payload2") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + assert.Equal(t, 2, len(response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf)) + assert.Equal(t, 5, len(operation.parser.openAPI.Components.Spec.Schemas)) + + assert.Equal(t, "#/components/schemas/model.Payload", operation.parser.openAPI.Components.Spec.Schemas["data"].Spec.Properties["data"].Ref.Ref) + assert.Equal(t, "#/components/schemas/model.Payload2", operation.parser.openAPI.Components.Spec.Schemas["data2"].Spec.Properties["data2"].Ref.Ref) +} + +func TestParseResponseCommentWithNestedArrayObjectTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data=[]model.Payload,data2=[]model.Payload2} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.Payload2") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + + allOf := response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf + assert.Equal(t, 2, len(allOf)) + + assert.Equal(t, "#/components/schemas/model.Payload", operation.parser.openAPI.Components.Spec.Schemas["data"].Spec.Properties["data"].Spec.Items.Schema.Ref.Ref) + assert.Equal(t, typeArray, operation.parser.openAPI.Components.Spec.Schemas["data"].Spec.Properties["data"].Spec.Type) + + assert.Equal(t, "#/components/schemas/model.Payload2", operation.parser.openAPI.Components.Spec.Schemas["data2"].Spec.Properties["data2"].Spec.Items.Schema.Ref.Ref) + assert.Equal(t, typeArray, operation.parser.openAPI.Components.Spec.Schemas["data2"].Spec.Properties["data2"].Spec.Type) +} + +func TestParseResponseCommentWithNestedFieldsV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data1=int,data2=[]int,data3=model.Payload,data4=[]model.Payload} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + + allOf := response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf + assert.Equal(t, 4, len(allOf)) + + schemas := operation.parser.openAPI.Components.Spec.Schemas + + assert.Equal(t, typeInteger, schemas["data1"].Spec.Properties["data1"].Spec.Type) + assert.Equal(t, typeObject, schemas["data1"].Spec.Type) + + assert.Equal(t, typeArray, schemas["data2"].Spec.Properties["data2"].Spec.Type) + assert.Equal(t, typeInteger, schemas["data2"].Spec.Properties["data2"].Spec.Items.Schema.Spec.Type) + assert.Equal(t, typeObject, schemas["data2"].Spec.Type) + + assert.Equal(t, "#/components/schemas/model.Payload", schemas["data3"].Spec.Properties["data3"].Ref.Ref) + assert.Equal(t, typeObject, schemas["data3"].Spec.Type) + + assert.Equal(t, "#/components/schemas/model.Payload", schemas["data4"].Spec.Properties["data4"].Spec.Items.Schema.Ref.Ref) + assert.Equal(t, typeArray, schemas["data4"].Spec.Properties["data4"].Spec.Type) + assert.Equal(t, typeObject, schemas["data4"].Spec.Type) +} + +func TestParseResponseCommentWithDeepNestedFieldsV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data1=int,data2=[]int,data3=model.Payload{data1=int,data2=model.DeepPayload},data4=[]model.Payload{data1=[]int,data2=[]model.DeepPayload}} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.DeepPayload") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + + allOf := response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf + assert.Equal(t, 4, len(allOf)) + + schemas := operation.parser.openAPI.Components.Spec.Schemas + + assert.Equal(t, typeInteger, schemas["data1"].Spec.Properties["data1"].Spec.Type) + assert.Equal(t, typeObject, schemas["data1"].Spec.Type) + + assert.Equal(t, typeArray, schemas["data2"].Spec.Properties["data2"].Spec.Type) + assert.Equal(t, typeInteger, schemas["data2"].Spec.Properties["data2"].Spec.Items.Schema.Spec.Type) + assert.Equal(t, typeObject, schemas["data2"].Spec.Type) + + assert.Equal(t, typeObject, schemas["data3"].Spec.Type) + assert.Equal(t, typeObject, schemas["data3"].Spec.Properties["data3"].Spec.Type) + assert.Equal(t, 2, len(schemas["data3"].Spec.Properties["data3"].Spec.AllOf)) + + assert.Equal(t, typeObject, schemas["data4"].Spec.Type) + assert.Equal(t, typeArray, schemas["data4"].Spec.Properties["data4"].Spec.Type) + assert.Equal(t, typeObject, schemas["data4"].Spec.Properties["data4"].Spec.Items.Schema.Spec.Type) + assert.Equal(t, 2, len(schemas["data4"].Spec.Properties["data4"].Spec.Items.Schema.Spec.AllOf)) +} + +func TestParseResponseCommentWithNestedArrayMapFieldsV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} []map[string]model.CommonHeader{data1=[]map[string]model.Payload,data2=map[string][]int} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + + content := response.Spec.Spec.Content["application/json"] + assert.NotNil(t, content) + assert.NotNil(t, content.Spec) + assert.NotNil(t, content.Spec.Schema.Spec.Items.Schema.Spec.AdditionalProperties.Schema) + + assert.Equal(t, 2, len(content.Spec.Schema.Spec.Items.Schema.Spec.AdditionalProperties.Schema.Spec.AllOf)) + assert.Equal(t, typeArray, content.Spec.Schema.Spec.Type) + assert.Equal(t, typeObject, content.Spec.Schema.Spec.Items.Schema.Spec.Type) + assert.Equal(t, typeObject, content.Spec.Schema.Spec.Items.Schema.Spec.AdditionalProperties.Schema.Spec.Type) + + schemas := operation.parser.openAPI.Components.Spec.Schemas + + data1 := schemas["data1"] + assert.NotNil(t, data1) + assert.NotNil(t, data1.Spec) + assert.NotNil(t, data1.Spec.Properties) + + assert.Equal(t, typeObject, data1.Spec.Type) + assert.Equal(t, typeArray, data1.Spec.Properties["data1"].Spec.Type) + assert.Equal(t, typeObject, data1.Spec.Properties["data1"].Spec.Items.Schema.Spec.Type) + assert.Equal(t, "#/components/schemas/model.Payload", data1.Spec.Properties["data1"].Spec.Items.Schema.Spec.AdditionalProperties.Schema.Ref.Ref) + + data2 := schemas["data2"] + assert.NotNil(t, data2) + assert.NotNil(t, data2.Spec) + assert.NotNil(t, data2.Spec.Properties) + + assert.Equal(t, typeObject, data2.Spec.Type) + assert.Equal(t, typeObject, data2.Spec.Properties["data2"].Spec.Type) + assert.Equal(t, typeArray, data2.Spec.Properties["data2"].Spec.AdditionalProperties.Schema.Spec.Type) + assert.Equal(t, typeInteger, data2.Spec.Properties["data2"].Spec.AdditionalProperties.Schema.Spec.Items.Schema.Spec.Type) + + commonHeader := schemas["model.CommonHeader"] + assert.NotNil(t, commonHeader) + assert.NotNil(t, commonHeader.Spec) + assert.Equal(t, 2, len(commonHeader.Spec.AllOf)) + assert.Equal(t, typeObject, commonHeader.Spec.Type) + + payload := schemas["model.Payload"] + assert.NotNil(t, payload) + assert.NotNil(t, payload.Spec) + assert.Equal(t, typeObject, payload.Spec.Type) +} + +func TestParseResponseCommentWithObjectTypeInSameFileV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} testOwner "Error message, if code != 200"` + operation := NewOperationV3(New()) + + operation.parser.addTestType("swag.testOwner") + + fset := token.NewFileSet() + astFile, err := goparser.ParseFile(fset, "operation_test.go", `package swag + type testOwner struct { + + } + `, goparser.ParseComments) + assert.NoError(t, err) + + err = operation.ParseComment(comment, astFile) + assert.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + assert.Equal(t, "#/components/schemas/swag.testOwner", response.Spec.Spec.Content["application/json"].Spec.Schema.Ref.Ref) +} + +func TestParseResponseCommentWithObjectTypeErrV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.OrderRow "Error message, if code != 200"` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.notexist") + + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseResponseCommentWithArrayTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {array} model.OrderRow "Error message, if code != 200` + operation := NewOperationV3(New()) + operation.parser.addTestType("model.OrderRow") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + assert.Equal(t, typeArray, response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) + assert.Equal(t, "#/components/schemas/model.OrderRow", response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Items.Schema.Ref.Ref) + +} + +func TestParseResponseCommentWithBasicTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {string} string "it's ok'"` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok'", response.Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) +} + +func TestParseResponseCommentWithBasicTypeAndCodesV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200,201,default {string} string "it's ok"` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Response["201"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Default + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) +} + +func TestParseEmptyResponseCommentV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 "it is ok"` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it is ok", response.Spec.Spec.Description) +} + +func TestParseEmptyResponseCommentWithCodesV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200,201,default "it is ok"` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it is ok", response.Spec.Spec.Description) + + response = operation.Responses.Spec.Response["201"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it is ok", response.Spec.Spec.Description) + + response = operation.Responses.Spec.Default + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it is ok", response.Spec.Spec.Description) +} + +func TestParseResponseCommentWithHeaderV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + err := operation.ParseComment(`@Success 200 "it's ok"`, nil) + assert.NoError(t, err, "ParseComment should not fail") + + err = operation.ParseComment(`@Header 200 {string} Token "qwerty"`, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + err = operation.ParseComment(`@Header 200 "Mallformed"`, nil) + assert.Error(t, err, "ParseComment should fail") + + err = operation.ParseComment(`@Header 200,asdsd {string} Token "qwerty"`, nil) + assert.Error(t, err, "ParseComment should fail") +} + +func TestParseResponseCommentWithHeaderForCodesV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + + comment := `@Success 200,201,default "it's ok"` + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + comment = `@Header 200,201,default {string} Token "qwerty"` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + comment = `@Header all {string} Token2 "qwerty"` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token2"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token2"].Spec.Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Response["201"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token2"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token2"].Spec.Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Default + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token2"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token2"].Spec.Spec.Schema.Spec.Type) + + comment = `@Header 200 "Mallformed"` + err = operation.ParseComment(comment, nil) + assert.Error(t, err, "ParseComment should not fail") +} + +func TestParseResponseCommentWithHeaderOnlyAllV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + + comment := `@Success 200,201,default "it's ok"` + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + comment = `@Header all {string} Token "qwerty"` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Response["201"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Default + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + comment = `@Header 200 "Mallformed"` + err = operation.ParseComment(comment, nil) + assert.Error(t, err, "ParseComment should not fail") +} + +func TestParseEmptyResponseOnlyCodeV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + err := operation.ParseComment(`@Success 200`, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "OK", response.Spec.Spec.Description) +} + +func TestParseEmptyResponseOnlyCodesV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200,201,default` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "OK", response.Spec.Spec.Description) + + response = operation.Responses.Spec.Response["201"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "Created", response.Spec.Spec.Description) + + response = operation.Responses.Spec.Default + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "", response.Spec.Spec.Description) +} + +func TestParseResponseCommentParamMissingV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + + paramLenErrComment := `@Success notIntCode` + paramLenErr := operation.ParseComment(paramLenErrComment, nil) + assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode"`) + + paramLenErrComment = `@Success notIntCode {string} string "it ok"` + paramLenErr = operation.ParseComment(paramLenErrComment, nil) + assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode {string} string "it ok""`) + + paramLenErrComment = `@Success notIntCode "it ok"` + paramLenErr = operation.ParseComment(paramLenErrComment, nil) + assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode "it ok""`) +} + +func TestOperation_ParseParamCommentV3(t *testing.T) { + t.Parallel() + + t.Run("integer", func(t *testing.T) { + t.Parallel() + for _, paramType := range []string{"header", "path", "query", "formData"} { + t.Run(paramType, func(t *testing.T) { + o := NewOperationV3(New()) + err := o.ParseComment(`@Param some_id `+paramType+` int true "Some ID"`, nil) + + assert.NoError(t, err) + + expected := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ + Spec: &spec.Extendable[spec.Parameter]{ + Spec: &spec.Parameter{ + Name: "some_id", + Description: "Some ID", + In: paramType, + Required: true, + Schema: &spec.RefOrSpec[spec.Schema]{ + Spec: &spec.Schema{ + JsonSchema: spec.JsonSchema{ + JsonSchemaCore: spec.JsonSchemaCore{ + Type: typeInteger, + }, + }, + }, + }, + }, + }, + } + + expectedArray := []*spec.RefOrSpec[spec.Extendable[spec.Parameter]]{expected} + assert.Equal(t, o.Parameters, expectedArray) + }) + } + }) + + t.Run("string", func(t *testing.T) { + t.Parallel() + for _, paramType := range []string{"header", "path", "query", "formData"} { + t.Run(paramType, func(t *testing.T) { + o := NewOperationV3(New()) + err := o.ParseComment(`@Param some_string `+paramType+` string true "Some String"`, nil) + + assert.NoError(t, err) + expected := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ + Spec: &spec.Extendable[spec.Parameter]{ + Spec: &spec.Parameter{ + Description: "Some String", + Name: "some_string", + In: paramType, + Required: true, + Schema: &spec.RefOrSpec[spec.Schema]{ + Spec: &spec.Schema{ + JsonSchema: spec.JsonSchema{ + JsonSchemaCore: spec.JsonSchemaCore{ + Type: typeString, + }, + }, + }, + }, + }, + }, + } + + expectedArray := []*spec.RefOrSpec[spec.Extendable[spec.Parameter]]{expected} + assert.Equal(t, o.Parameters, expectedArray) + }) + } + }) + + t.Run("object", func(t *testing.T) { + t.Parallel() + for _, paramType := range []string{"header", "path", "query", "formData"} { + t.Run(paramType, func(t *testing.T) { + assert.Error(t, + NewOperationV3(New()). + ParseComment(`@Param some_object `+paramType+` main.Object true "Some Object"`, + nil)) + }) + } + }) + +} + +// Test ParseParamComment Query Params +func TestParseParamCommentBodyArrayV3(t *testing.T) { + t.Parallel() + + comment := `@Param names body []string true "Users List"` + o := NewOperationV3(New()) + err := o.ParseComment(comment, nil) + assert.NoError(t, err) + + expected := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ + Spec: &spec.Extendable[spec.Parameter]{ + Spec: &spec.Parameter{ + Name: "names", + Description: "Users List", + In: "body", + Required: true, + Schema: &spec.RefOrSpec[spec.Schema]{ + Spec: &spec.Schema{ + JsonSchema: spec.JsonSchema{ + JsonSchemaCore: spec.JsonSchemaCore{ + Type: typeArray, + }, + JsonSchemaTypeArray: spec.JsonSchemaTypeArray{ + Items: &spec.BoolOrSchema{ + Schema: &spec.RefOrSpec[spec.Schema]{ + Spec: &spec.Schema{ + JsonSchema: spec.JsonSchema{ + JsonSchemaCore: spec.JsonSchemaCore{ + Type: typeString, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + expectedArray := []*spec.RefOrSpec[spec.Extendable[spec.Parameter]]{expected} + assert.Equal(t, o.Parameters, expectedArray) + +} + +func TestParseParamCommentArrayV3(t *testing.T) { + paramTypes := []string{"header", "path", "query"} + + for _, paramType := range paramTypes { + t.Run(paramType, func(t *testing.T) { + operation := NewOperationV3(New()) + err := operation.ParseComment(`@Param names `+paramType+` []string true "Users List"`, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Users List", parameterSpec.Description) + assert.Equal(t, "names", parameterSpec.Name) + assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, paramType, parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Items.Schema.Spec.Type) + + err = operation.ParseComment(`@Param names `+paramType+` []model.User true "Users List"`, nil) + assert.Error(t, err) + }) + } +} + +func TestParseParamCommentDefaultValueV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(New()) + err := operation.ParseComment(`@Param names query string true "Users List" default(test)`, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Users List", parameterSpec.Description) + assert.Equal(t, "names", parameterSpec.Name) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, "test", parameterSpec.Schema.Spec.Default) +} + +func TestParseParamCommentQueryArrayFormatV3(t *testing.T) { + t.Parallel() + + comment := `@Param names query []string true "Users List" collectionFormat(multi)` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Users List", parameterSpec.Description) + assert.Equal(t, "names", parameterSpec.Name) + assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Items.Schema.Spec.Type) + assert.Equal(t, "form", parameterSpec.Style) + +} + +func TestParseParamCommentByIDV3(t *testing.T) { + t.Parallel() + + comment := `@Param unsafe_id[lte] query int true "Unsafe query param"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Unsafe query param", parameterSpec.Description) + assert.Equal(t, "unsafe_id[lte]", parameterSpec.Name) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) +} + +func TestParseParamCommentByQueryTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query int true "Some ID"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) +} + +func TestParseParamCommentByBodyTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id body model.OrderRow true "Some ID"` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.OrderRow") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, "#/components/schemas/model.OrderRow", parameterSpec.Schema.Ref.Ref) +} + +func TestParseParamCommentByBodyTextPlainV3(t *testing.T) { + t.Parallel() + + comment := `@Param text body string true "Text to process"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Text to process", parameterSpec.Description) + assert.Equal(t, "text", parameterSpec.Name) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) +} + +func TestParseParamCommentByBodyTypeWithDeepNestedFieldsV3(t *testing.T) { + t.Parallel() + + comment := `@Param body body model.CommonHeader{data=string,data2=int} true "test deep"` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Len(t, operation.Parameters, 1) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "test deep", parameterSpec.Description) + assert.Equal(t, "body", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + + assert.Equal(t, 2, len(parameterSpec.Schema.Spec.AllOf)) + assert.Equal(t, 3, len(operation.parser.openAPI.Components.Spec.Schemas)) +} + +func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id body []int true "Some ID"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Items.Schema.Spec.Type) +} + +func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoWithDeepNestedFieldsV3(t *testing.T) { + t.Parallel() + + comment := `@Param body body []model.CommonHeader{data=string,data2=int} true "test deep"` + operation := NewOperationV3(New()) + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Len(t, operation.Parameters, 1) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "test deep", parameterSpec.Description) + assert.Equal(t, "body", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 2, len(parameterSpec.Schema.Spec.Items.Schema.Spec.AllOf)) +} + +func TestParseParamCommentByBodyTypeErrV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id body model.OrderRow true "Some ID"` + operation := NewOperationV3(New()) + operation.parser.addTestType("model.notexist") + + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseParamCommentByFormDataTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Param file formData file true "this is a test file"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Len(t, operation.Parameters, 1) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "this is a test file", parameterSpec.Description) + assert.Equal(t, "file", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "formData", parameterSpec.In) + assert.Equal(t, typeFile, parameterSpec.Schema.Spec.Type) +} + +func TestParseParamCommentByFormDataTypeUint64V3(t *testing.T) { + t.Parallel() + + comment := `@Param file formData uint64 true "this is a test file"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Len(t, operation.Parameters, 1) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "this is a test file", parameterSpec.Description) + assert.Equal(t, "file", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "formData", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) +} + +func TestParseParamCommentByNotSupportedTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id not_supported int true "Some ID"` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + + assert.Error(t, err) +} + +func TestParseParamCommentNotMatchV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id body mock true` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + + assert.Error(t, err) +} + +func TestParseParamCommentByEnumsV3(t *testing.T) { + t.Parallel() + + t.Run("string", func(t *testing.T) { + comment := `@Param some_id query string true "Some ID" Enums(A, B, C)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Len(t, operation.Parameters, 1) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 3, len(parameterSpec.Schema.Spec.Enum)) + + enums := []interface{}{"A", "B", "C"} + assert.EqualValues(t, enums, parameterSpec.Schema.Spec.Enum) + }) + + t.Run("int", func(t *testing.T) { + comment := `@Param some_id query int true "Some ID" Enums(1, 2, 3)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 3, len(parameterSpec.Schema.Spec.Enum)) + + enums := []interface{}{1, 2, 3} + assert.EqualValues(t, enums, parameterSpec.Schema.Spec.Enum) + }) + + t.Run("number", func(t *testing.T) { + comment := `@Param some_id query number true "Some ID" Enums(1.1, 2.2, 3.3)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeNumber, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 3, len(parameterSpec.Schema.Spec.Enum)) + + enums := []interface{}{1.1, 2.2, 3.3} + assert.EqualValues(t, enums, parameterSpec.Schema.Spec.Enum) + }) + + t.Run("bool", func(t *testing.T) { + comment := `@Param some_id query bool true "Some ID" Enums(true, false)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeBool, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 2, len(parameterSpec.Schema.Spec.Enum)) + + enums := []interface{}{true, false} + assert.EqualValues(t, enums, parameterSpec.Schema.Spec.Enum) + }) + + operation := NewOperationV3(New()) + + comment := `@Param some_id query int true "Some ID" Enums(A, B, C)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query number true "Some ID" Enums(A, B, C)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query boolean true "Some ID" Enums(A, B, C)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query Document true "Some ID" Enums(A, B, C)` + assert.Error(t, operation.ParseComment(comment, nil)) +} + +func TestParseParamCommentByMaxLengthV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query string true "Some ID" MaxLength(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, *parameterSpec.Schema.Spec.MaxLength) + + comment = `@Param some_id query int true "Some ID" MaxLength(10)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query string true "Some ID" MaxLength(Goopher)` + assert.Error(t, operation.ParseComment(comment, nil)) +} + +func TestParseParamCommentByMinLengthV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query string true "Some ID" MinLength(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, *parameterSpec.Schema.Spec.MinLength) + + comment = `@Param some_id query int true "Some ID" MinLength(10)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query string true "Some ID" MinLength(Goopher)` + assert.Error(t, operation.ParseComment(comment, nil)) +} + +func TestParseParamCommentByMinimumV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query int true "Some ID" Minimum(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, *parameterSpec.Schema.Spec.Minimum) + + comment = `@Param some_id query int true "Some ID" Mininum(10)` + assert.NoError(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query string true "Some ID" Minimum(10)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query integer true "Some ID" Minimum(Goopher)` + assert.Error(t, operation.ParseComment(comment, nil)) +} + +func TestParseParamCommentByMaximumV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query int true "Some ID" Maximum(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, *parameterSpec.Schema.Spec.Maximum) + + comment = `@Param some_id query int true "Some ID" Maxinum(10)` + assert.NoError(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query string true "Some ID" Maximum(10)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query integer true "Some ID" Maximum(Goopher)` + assert.Error(t, operation.ParseComment(comment, nil)) +} + +func TestParseParamCommentByDefaultV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query int true "Some ID" Default(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, parameterSpec.Schema.Spec.Default) +} + +func TestParseParamCommentByExampleIntV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query int true "Some ID" Example(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, parameterSpec.Example) +} + +func TestParseParamCommentByExampleStringV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query string true "Some ID" Example(True feelings)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + assert.Equal(t, "True feelings", parameterSpec.Example) +} + +func TestParseParamCommentByExampleUnsupportedTypeV3(t *testing.T) { + t.Parallel() + var param spec.Parameter + + setExampleV3(¶m, "something", "random value") + assert.Equal(t, param.Example, nil) + + setExampleV3(¶m, STRING, "string value") + assert.Equal(t, param.Example, "string value") + + setExampleV3(¶m, INTEGER, "10") + assert.Equal(t, param.Example, 10) + + setExampleV3(¶m, NUMBER, "10") + assert.Equal(t, param.Example, float64(10)) +} + +func TestParseParamCommentBySchemaExampleStringV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id body string true "Some ID" SchemaExample(True feelings)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, "True feelings", parameterSpec.Schema.Spec.Example) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) +} + +func TestParseParamCommentBySchemaExampleUnsupportedTypeV3(t *testing.T) { + t.Parallel() + var param spec.Parameter + + setSchemaExampleV3(¶m, "something", "random value") + assert.Nil(t, param.Schema) + + setSchemaExampleV3(¶m, STRING, "string value") + assert.Nil(t, param.Schema) + + param.Schema = spec.NewSchemaSpec() + setSchemaExampleV3(¶m, STRING, "string value") + assert.Equal(t, "string value", param.Schema.Spec.Example) + + setSchemaExampleV3(¶m, INTEGER, "10") + assert.Equal(t, 10, param.Schema.Spec.Example) + + setSchemaExampleV3(¶m, NUMBER, "10") + assert.Equal(t, float64(10), param.Schema.Spec.Example) + + setSchemaExampleV3(¶m, STRING, "string \\r\\nvalue") + assert.Equal(t, "string \r\nvalue", param.Schema.Spec.Example) +} + +func TestParseParamArrayWithEnumsV3(t *testing.T) { + t.Parallel() + + comment := `@Param field query []string true "An enum collection" collectionFormat(csv) enums(also,valid)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "An enum collection", parameterSpec.Description) + assert.Equal(t, "field", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.Equal(t, "form", parameterSpec.Style) + + enums := []interface{}{"also", "valid"} + assert.EqualValues(t, enums, parameterSpec.Schema.Spec.Items.Schema.Spec.Enum) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Items.Schema.Spec.Type) +} + +func TestParseAndExtractionParamAttributeV3(t *testing.T) { + t.Parallel() + + op := NewOperationV3(New()) + + t.Run("number", func(t *testing.T) { + numberParam := spec.Parameter{ + Schema: spec.NewSchemaSpec(), + } + err := op.parseParamAttribute( + " default(1) maximum(100) minimum(0) format(csv)", + "", + NUMBER, + &numberParam, + ) + assert.NoError(t, err) + assert.Equal(t, int(0), *numberParam.Schema.Spec.Minimum) + assert.Equal(t, int(100), *numberParam.Schema.Spec.Maximum) + assert.Equal(t, "csv", numberParam.Schema.Spec.Format) + assert.Equal(t, float64(1), numberParam.Schema.Spec.Default) + + err = op.parseParamAttribute(" minlength(1)", "", NUMBER, nil) + assert.Error(t, err) + + err = op.parseParamAttribute(" maxlength(1)", "", NUMBER, nil) + assert.Error(t, err) + }) + + t.Run("string", func(t *testing.T) { + stringParam := spec.Parameter{ + Schema: spec.NewSchemaSpec(), + } + err := op.parseParamAttribute( + " default(test) maxlength(100) minlength(0) format(csv)", + "", + STRING, + &stringParam, + ) + assert.NoError(t, err) + assert.Equal(t, int(0), *stringParam.Schema.Spec.MinLength) + assert.Equal(t, int(100), *stringParam.Schema.Spec.MaxLength) + assert.Equal(t, "csv", stringParam.Schema.Spec.Format) + err = op.parseParamAttribute(" minimum(0)", "", STRING, nil) + assert.Error(t, err) + + err = op.parseParamAttribute(" maximum(0)", "", STRING, nil) + assert.Error(t, err) + }) + + t.Run("array", func(t *testing.T) { + arrayParam := spec.Parameter{ + Schema: spec.NewSchemaSpec(), + } + + arrayParam.In = "path" + err := op.parseParamAttribute(" collectionFormat(simple)", ARRAY, STRING, &arrayParam) + assert.Equal(t, "simple", arrayParam.Style) + assert.NoError(t, err) + + err = op.parseParamAttribute(" collectionFormat(simple)", STRING, STRING, nil) + assert.Error(t, err) + + err = op.parseParamAttribute(" default(0)", "", ARRAY, nil) + assert.NoError(t, err) + }) +} + +func TestParseParamCommentByExtensionsV3(t *testing.T) { + comment := `@Param some_id path int true "Some ID" extensions(x-example=test,x-custom=Gopher,x-custom2)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.Equal(t, "path", parameterSpec.In) + assert.True(t, parameterSpec.Required) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, "Gopher", parameterSpec.Schema.Spec.Extensions["x-custom"]) + assert.Equal(t, true, parameterSpec.Schema.Spec.Extensions["x-custom2"]) + assert.Equal(t, "test", parameterSpec.Schema.Spec.Extensions["x-example"]) +} + +func TestParseIdCommentV3(t *testing.T) { + t.Parallel() + + comment := `@Id myOperationId` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + + assert.NoError(t, err) + assert.Equal(t, "myOperationId", operation.Operation.OperationID) +} + +func TestParseSecurityCommentV3(t *testing.T) { + t.Parallel() + + comment := `@Security OAuth2Implicit[read, write]` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + expected := []spec.SecurityRequirement{{ + "OAuth2Implicit": {"read", "write"}, + }} + + assert.Equal(t, expected, operation.Security) +} + +func TestParseSecurityCommentSimpleV3(t *testing.T) { + t.Parallel() + + comment := `@Security ApiKeyAuth` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + expected := []spec.SecurityRequirement{{ + "ApiKeyAuth": {}, + }} + + assert.Equal(t, expected, operation.Security) +} + +func TestParseSecurityCommentOrV3(t *testing.T) { + t.Parallel() + + comment := `@Security OAuth2Implicit[read, write] || Firebase[]` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + expected := []spec.SecurityRequirement{{ + "OAuth2Implicit": {"read", "write"}, + "Firebase": {""}, + }} + + assert.Equal(t, expected, operation.Security) +} + +func TestParseMultiDescriptionV3(t *testing.T) { + t.Parallel() + + comment := `@Description line one` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + comment = `@Tags multi` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err) + + comment = `@Description line two x` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Equal(t, "line one\nline two x", operation.Description) +} + +func TestParseDescriptionMarkdownV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(New()) + operation.parser.markdownFileDir = "example/markdown" + + comment := `@description.markdown admin.md` + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + comment = `@description.markdown missing.md` + + err = operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseSummaryV3(t *testing.T) { + t.Parallel() + + comment := `@summary line one` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + assert.Equal(t, "line one", operation.Summary) + + comment = `@Summary line one` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err) +} + +func TestParseDeprecationDescriptionV3(t *testing.T) { + t.Parallel() + + comment := `@Deprecated` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + assert.True(t, operation.Deprecated) +} + +func TestParseExtensionsV3(t *testing.T) { + t.Parallel() + // Fail if there are no args for attributes. + { + comment := `@x-amazon-apigateway-integration` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a value") + } + + // Fail if args of attributes are broken. + { + comment := `@x-amazon-apigateway-integration ["broken"}]` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a valid json value. error: invalid character '}' after array element") + } + + // OK + { + comment := `@x-amazon-apigateway-integration {"uri": "${some_arn}", "passthroughBehavior": "when_no_match", "httpMethod": "POST", "type": "aws_proxy"}` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + assert.Equal(t, map[string]interface{}{ + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": "${some_arn}", + }, operation.Responses.Extensions["x-amazon-apigateway-integration"]) + } + + // Test x-tagGroups + { + comment := `@x-tagGroups [{"name":"Natural Persons","tags":["Person","PersonRisk","PersonDocuments"]}]` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + assert.Equal(t, + []interface{}{map[string]interface{}{ + "name": "Natural Persons", + "tags": []interface{}{ + "Person", + "PersonRisk", + "PersonDocuments", + }, + }}, operation.Responses.Extensions["x-tagGroups"]) + } +} + +func TestParseResponseHeaderCommentV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(New()) + + err := operation.ParseResponseComment(`default {string} string "other error"`, nil) + assert.NoError(t, err) + err = operation.ParseResponseHeaderComment(`all {string} Token "qwerty"`, nil) + assert.NoError(t, err) +} + +func TestParseCodeSamplesV3(t *testing.T) { + t.Parallel() + const comment = `@x-codeSamples file` + t.Run("Find sample by file", func(t *testing.T) { + + operation := NewOperationV3(New(), SetCodeExampleFilesDirectoryV3("testdata/code_examples")) + operation.Summary = "example" + + err := operation.ParseComment(comment, nil) + require.NoError(t, err, "no error should be thrown") + + assert.Equal(t, "example", operation.Summary) + assert.Equal(t, CodeSamples(CodeSamples{map[string]string{"lang": "JavaScript", "source": "console.log('Hello World');"}}), + operation.Responses.Extensions["x-codeSamples"], + ) + }) + + t.Run("With broken file sample", func(t *testing.T) { + operation := NewOperationV3(New(), SetCodeExampleFilesDirectoryV3("testdata/code_examples")) + operation.Summary = "broken" + + err := operation.ParseComment(comment, nil) + assert.Error(t, err, "no error should be thrown") + }) + + t.Run("Example file not found", func(t *testing.T) { + operation := NewOperationV3(New(), SetCodeExampleFilesDirectoryV3("testdata/code_examples")) + operation.Summary = "badExample" + + err := operation.ParseComment(comment, nil) + assert.Error(t, err, "error was expected, as file does not exist") + }) + + t.Run("Without line reminder", func(t *testing.T) { + comment := `@x-codeSamples` + operation := NewOperationV3(New(), SetCodeExampleFilesDirectoryV3("testdata/code_examples")) + operation.Summary = "example" + + err := operation.ParseComment(comment, nil) + assert.Error(t, err, "no error should be thrown") + }) + + t.Run(" broken dir", func(t *testing.T) { + operation := NewOperationV3(New(), SetCodeExampleFilesDirectoryV3("testdata/fake_examples")) + operation.Summary = "code" + + err := operation.ParseComment(comment, nil) + assert.Error(t, err, "no error should be thrown") + }) +} diff --git a/packages.go b/packages.go index 2c5693fc4..4bf31b751 100644 --- a/packages.go +++ b/packages.go @@ -133,6 +133,7 @@ func (pkgDefs *PackagesDefinitions) parseTypesFromFile(astFile *ast.File, packag if !ok { continue } + if generalDeclaration.Tok == token.TYPE { for _, astSpec := range generalDeclaration.Specs { if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { @@ -245,6 +246,7 @@ func (pkgDefs *PackagesDefinitions) parseFunctionScopedTypesFromFile(astFile *as } } + } } } @@ -548,7 +550,18 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File } pkgPaths, externalPkgPaths := pkgDefs.findPackagePathFromImports(parts[0], file) - typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, parts[1]) + + if len(pkgPaths) == 0 && len(externalPkgPaths) == 0 { + pkgDefinition := pkgDefs.packages["pkg/"+parts[0]] + if pkgDefinition == nil { + return pkgDefs.findTypeSpec("", parts[1]) + } + + typeDef = pkgDefinition.TypeDefinitions[parts[1]] + } else { + typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, parts[1]) + } + return pkgDefs.parametrizeGenericType(file, typeDef, typeName) } @@ -567,7 +580,32 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File typeDef, ok = pkgDefs.uniqueDefinitions[fullTypeName(file.Name.Name, name)] if !ok { pkgPaths, externalPkgPaths := pkgDefs.findPackagePathFromImports("", file) - typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, name) + + if len(pkgPaths) == 0 { + pkgDefinition := pkgDefs.packages["pkg/"+parts[0]] + if pkgDefinition == nil { + return pkgDefs.findTypeSpec("", parts[1]) + } + + typeDef = pkgDefinition.TypeDefinitions[parts[0]] + } else { + typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, name) + } } + return pkgDefs.parametrizeGenericType(file, typeDef, typeName) } + +func isAliasPkgName(file *ast.File, pkgName string) bool { + if file == nil && file.Imports == nil { + return false + } + + for _, pkg := range file.Imports { + if pkg.Name != nil && pkg.Name.Name == pkgName { + return true + } + } + + return false +} diff --git a/packages_test.go b/packages_test.go index 595659ba1..b1734fc12 100644 --- a/packages_test.go +++ b/packages_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestPackagesDefinitions_ParseFile(t *testing.T) { +func Test_PackagesDefinitions_ParseFile(t *testing.T) { pd := PackagesDefinitions{} packageDir := "github.com/swaggo/swag/testdata/simple" assert.NoError(t, pd.ParseFile(packageDir, "testdata/simple/main.go", nil, ParseAll)) diff --git a/parser.go b/parser.go index 142c53c55..3c45972e7 100644 --- a/parser.go +++ b/parser.go @@ -3,7 +3,6 @@ package swag import ( "context" "encoding/json" - "errors" "fmt" "go/ast" "go/build" @@ -19,8 +18,11 @@ import ( "strconv" "strings" + "github.com/pkg/errors" + "github.com/KyleBanks/depth" "github.com/go-openapi/spec" + openapi "github.com/sv-tools/openapi/spec" ) const ( @@ -33,39 +35,40 @@ const ( // SnakeCase indicates using SnakeCase strategy for struct field. SnakeCase = "snakecase" - idAttr = "@id" - acceptAttr = "@accept" - produceAttr = "@produce" - paramAttr = "@param" - successAttr = "@success" - failureAttr = "@failure" - responseAttr = "@response" - headerAttr = "@header" - tagsAttr = "@tags" - routerAttr = "@router" - summaryAttr = "@summary" - deprecatedAttr = "@deprecated" - securityAttr = "@security" - titleAttr = "@title" - conNameAttr = "@contact.name" - conURLAttr = "@contact.url" - conEmailAttr = "@contact.email" - licNameAttr = "@license.name" - licURLAttr = "@license.url" - versionAttr = "@version" - descriptionAttr = "@description" - descriptionMarkdownAttr = "@description.markdown" - secBasicAttr = "@securitydefinitions.basic" - secAPIKeyAttr = "@securitydefinitions.apikey" - secApplicationAttr = "@securitydefinitions.oauth2.application" - secImplicitAttr = "@securitydefinitions.oauth2.implicit" - secPasswordAttr = "@securitydefinitions.oauth2.password" - secAccessCodeAttr = "@securitydefinitions.oauth2.accesscode" - tosAttr = "@termsofservice" - extDocsDescAttr = "@externaldocs.description" - extDocsURLAttr = "@externaldocs.url" - xCodeSamplesAttr = "@x-codesamples" - scopeAttrPrefix = "@scope." + idAttr = "@id" + acceptAttr = "@accept" + produceAttr = "@produce" + paramAttr = "@param" + successAttr = "@success" + failureAttr = "@failure" + responseAttr = "@response" + headerAttr = "@header" + tagsAttr = "@tags" + routerAttr = "@router" + summaryAttr = "@summary" + deprecatedAttr = "@deprecated" + securityAttr = "@security" + titleAttr = "@title" + conNameAttr = "@contact.name" + conURLAttr = "@contact.url" + conEmailAttr = "@contact.email" + licNameAttr = "@license.name" + licURLAttr = "@license.url" + versionAttr = "@version" + descriptionAttr = "@description" + descriptionMarkdownAttr = "@description.markdown" + secBasicAttr = "@securitydefinitions.basic" + secAPIKeyAttr = "@securitydefinitions.apikey" + secApplicationAttr = "@securitydefinitions.oauth2.application" + secImplicitAttr = "@securitydefinitions.oauth2.implicit" + secPasswordAttr = "@securitydefinitions.oauth2.password" + secAccessCodeAttr = "@securitydefinitions.oauth2.accesscode" + tosAttr = "@termsofservice" + extDocsDescAttr = "@externaldocs.description" + extDocsURLAttr = "@externaldocs.url" + xCodeSamplesAttr = "@x-codesamples" + xCodeSamplesAttrOriginal = "@x-codeSamples" + scopeAttrPrefix = "@scope." ) // ParseFlag determine what to parse @@ -111,15 +114,24 @@ type Parser struct { // swagger represents the root document object for the API specification swagger *spec.Swagger + // openAPI represents the v3.1 root document object for the API specification + openAPI *openapi.OpenAPI + // packages store entities of APIs, definitions, file, package path etc. and their relations packages *PackagesDefinitions // parsedSchemas store schemas which have been parsed from ast.TypeSpec parsedSchemas map[*TypeSpecDef]*Schema + // parsedSchemasV3 store schemas which have been parsed from ast.TypeSpec + parsedSchemasV3 map[*TypeSpecDef]*SchemaV3 + // outputSchemas store schemas which will be export to swagger outputSchemas map[*TypeSpecDef]*Schema + // outputSchemas store schemas which will be export to swagger + outputSchemasV3 map[*TypeSpecDef]*SchemaV3 + // PropNamingStrategy naming strategy PropNamingStrategy string @@ -162,6 +174,9 @@ type Parser struct { // fieldParserFactory create FieldParser fieldParserFactory FieldParserFactory + // fieldParserFactoryV3 create FieldParser + fieldParserFactoryV3 FieldParserFactoryV3 + // Overrides allows global replacements of types. A blank replacement will be skipped. Overrides map[string]string @@ -170,6 +185,9 @@ type Parser struct { // tags to filter the APIs after tags map[string]struct{} + + // use new openAPI version + openAPIVersion bool } // FieldParserFactory create FieldParser. @@ -214,17 +232,31 @@ func New(options ...func(*Parser)) *Parser { SecurityDefinitions: make(map[string]*spec.SecurityScheme), }, VendorExtensible: spec.VendorExtensible{ - Extensions: nil, + Extensions: make(spec.Extensions), }, }, - packages: NewPackagesDefinitions(), - debug: log.New(os.Stdout, "", log.LstdFlags), - parsedSchemas: make(map[*TypeSpecDef]*Schema), - outputSchemas: make(map[*TypeSpecDef]*Schema), - excludes: make(map[string]struct{}), - tags: make(map[string]struct{}), - fieldParserFactory: newTagBaseFieldParser, - Overrides: make(map[string]string), + openAPI: &openapi.OpenAPI{ + Info: openapi.NewInfo(), + OpenAPI: "3.1.0", + Components: openapi.NewComponents(), + ExternalDocs: openapi.NewExternalDocs(), + Paths: openapi.NewPaths(), + WebHooks: map[string]*openapi.RefOrSpec[openapi.Extendable[openapi.PathItem]]{}, + Security: []openapi.SecurityRequirement{}, + Tags: []*openapi.Extendable[openapi.Tag]{}, + Servers: []*openapi.Extendable[openapi.Server]{}, + }, + packages: NewPackagesDefinitions(), + debug: log.New(os.Stdout, "", log.LstdFlags), + parsedSchemas: make(map[*TypeSpecDef]*Schema), + parsedSchemasV3: make(map[*TypeSpecDef]*SchemaV3), + outputSchemas: make(map[*TypeSpecDef]*Schema), + outputSchemasV3: make(map[*TypeSpecDef]*SchemaV3), + excludes: make(map[string]struct{}), + tags: make(map[string]struct{}), + fieldParserFactory: newTagBaseFieldParser, + fieldParserFactoryV3: newTagBaseFieldParserV3, + Overrides: make(map[string]string), } for _, option := range options { @@ -338,6 +370,13 @@ func ParseUsingGoList(enabled bool) func(parser *Parser) { } } +// SetOpenAPIVersion parses only those operations which match given extension +func SetOpenAPIVersion(openAPIVersion bool) func(*Parser) { + return func(p *Parser) { + p.openAPIVersion = openAPIVersion + } +} + // ParseAPI parses general api info for given searchDir and mainAPIFile. func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string, parseDepth int) error { return parser.ParseAPIMultiSearchDir([]string{searchDir}, mainAPIFile, parseDepth) @@ -366,40 +405,7 @@ func (parser *Parser) ParseAPIMultiSearchDir(searchDirs []string, mainAPIFile st // Use 'go list' command instead of depth.Resolve() if parser.ParseDependency { - if parser.parseGoList { - pkgs, err := listPackages(context.Background(), filepath.Dir(absMainAPIFilePath), nil, "-deps") - if err != nil { - return fmt.Errorf("pkg %s cannot find all dependencies, %s", filepath.Dir(absMainAPIFilePath), err) - } - - length := len(pkgs) - for i := 0; i < length; i++ { - err := parser.getAllGoFileInfoFromDepsByList(pkgs[i]) - if err != nil { - return err - } - } - } else { - var t depth.Tree - t.ResolveInternal = true - t.MaxDepth = parseDepth - - pkgName, err := getPkgName(filepath.Dir(absMainAPIFilePath)) - if err != nil { - return err - } - - err = t.Resolve(pkgName) - if err != nil { - return fmt.Errorf("pkg %s cannot find all dependencies, %s", pkgName, err) - } - for i := 0; i < len(t.Root.Deps); i++ { - err := parser.getAllGoFileInfoFromDeps(&t.Root.Deps[i]) - if err != nil { - return err - } - } - } + parser.parseDeps(absMainAPIFilePath, parseDepth) } err = parser.ParseGeneralAPIInfo(absMainAPIFilePath) @@ -412,14 +418,59 @@ func (parser *Parser) ParseAPIMultiSearchDir(searchDirs []string, mainAPIFile st return err } - err = parser.packages.RangeFiles(parser.ParseRouterAPIInfo) - if err != nil { - return err + if parser.openAPIVersion { + err = parser.packages.RangeFiles(parser.ParseRouterAPIInfoV3) + if err != nil { + return err + } + } else { + err = parser.packages.RangeFiles(parser.ParseRouterAPIInfo) + if err != nil { + return err + } } return parser.checkOperationIDUniqueness() } +func (parser *Parser) parseDeps(absMainAPIFilePath string, parseDepth int) error { + if parser.parseGoList { + pkgs, err := listPackages(context.Background(), filepath.Dir(absMainAPIFilePath), nil, "-deps") + if err != nil { + return fmt.Errorf("pkg %s cannot find all dependencies, %s", filepath.Dir(absMainAPIFilePath), err) + } + + length := len(pkgs) + for i := 0; i < length; i++ { + err := parser.getAllGoFileInfoFromDepsByList(pkgs[i]) + if err != nil { + return err + } + } + } else { + var t depth.Tree + t.ResolveInternal = true + t.MaxDepth = parseDepth + + pkgName, err := getPkgName(absMainAPIFilePath) + if err != nil { + return errors.Wrap(err, "could not parse dependencies") + } + + if err := t.Resolve(pkgName); err != nil { + return errors.Wrap(fmt.Errorf("pkg %s cannot find all dependencies, %s", pkgName, err), "could not resolve dependencies") + } + + for i := 0; i < len(t.Root.Deps); i++ { + if err := parser.getAllGoFileInfoFromDeps(&t.Root.Deps[i]); err != nil { + return errors.Wrap(err, "could not parse dependencies") + } + } + } + + return nil +} + func getPkgName(searchDir string) (string, error) { cmd := exec.Command("go", "list", "-f={{.ImportPath}}") cmd.Dir = searchDir @@ -429,6 +480,8 @@ func getPkgName(searchDir string) (string, error) { cmd.Stdout = &stdout cmd.Stderr = &stderr + fmt.Println("get pkg name for directory:", searchDir) + if err := cmd.Run(); err != nil { return "", fmt.Errorf("execute go list command, %s, stdout:%s, stderr:%s", err, stdout.String(), stderr.String()) } @@ -448,16 +501,30 @@ func getPkgName(searchDir string) (string, error) { // ParseGeneralAPIInfo parses general api info for given mainAPIFile path. func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error { - fileTree, err := goparser.ParseFile(token.NewFileSet(), mainAPIFile, nil, goparser.ParseComments) + fileSet := token.NewFileSet() + filePath := mainAPIFile + + fileTree, err := goparser.ParseFile(fileSet, filePath, nil, goparser.ParseComments) if err != nil { - return fmt.Errorf("cannot parse source files %s: %s", mainAPIFile, err) + return fmt.Errorf("cannot parse source files %s: %s", filePath, err) } parser.swagger.Swagger = "2.0" - for _, comment := range fileTree.Comments { + for i := range fileTree.Comments { + comment := fileTree.Comments[i] + if !isGeneralAPIComment(comment.Text()) { + continue + } + comments := strings.Split(comment.Text(), "\n") - if !isGeneralAPIComment(comments) { + + if parser.openAPIVersion { + err = parser.parseGeneralAPIInfoV3(comments) + if err != nil { + return err + } + continue } @@ -465,6 +532,7 @@ func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error { if err != nil { return err } + } return nil @@ -583,6 +651,18 @@ func parseGeneralAPIInfo(parser *Parser, comments []string) error { parser.swagger.ExternalDocs.URL = value } + case "@x-taggroups": + originalAttribute := strings.Split(commentLine, " ")[0] + if len(value) == 0 { + return fmt.Errorf("annotation %s need a value", attribute) + } + + var valueJSON interface{} + if err := json.Unmarshal([]byte(value), &valueJSON); err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", originalAttribute, err.Error()) + } + + parser.swagger.Extensions[originalAttribute[1:]] = valueJSON // don't use the method provided by spec lib, cause it will call toLower() on attribute names, which is wrongy default: if strings.HasPrefix(attribute, "@x-") { extensionName := attribute[1:] @@ -610,7 +690,7 @@ func parseGeneralAPIInfo(parser *Parser, comments []string) error { var valueJSON interface{} err := json.Unmarshal([]byte(value), &valueJSON) if err != nil { - return fmt.Errorf("annotation %s need a valid json value", attribute) + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) } if strings.Contains(extensionName, "logo") { @@ -695,13 +775,14 @@ func parseSecAttributes(context string, lines []string, index *int) (*spec.Secur fields := FieldsByAnySpace(v, 2) securityAttr := strings.ToLower(fields[0]) + var value string if len(fields) > 1 { value = fields[1] } - for _, findterm := range search { - if securityAttr == findterm { + for _, findTerm := range search { + if securityAttr == findTerm { attrMap[securityAttr] = value break @@ -786,19 +867,20 @@ func (parser *Parser) ParseProduceComment(commentLine string) error { return parseMimeTypeList(commentLine, &parser.swagger.Produces, "%v produce type can't be accepted") } -func isGeneralAPIComment(comments []string) bool { - for _, commentLine := range comments { - commentLine = strings.TrimSpace(commentLine) - if len(commentLine) == 0 { - continue - } - attribute := strings.ToLower(FieldsByAnySpace(commentLine, 2)[0]) - switch attribute { - // The @summary, @router, @success, @failure annotation belongs to Operation - case summaryAttr, routerAttr, successAttr, failureAttr, responseAttr: - return false - } +func isGeneralAPIComment(comment string) bool { + // for _, commentLine := range comments { + commentLine := strings.TrimSpace(comment) + if len(commentLine) == 0 { + return false + } + + attribute := strings.ToLower(FieldsByAnySpace(commentLine, 2)[0]) + switch attribute { + // The @summary, @router, @success, @failure annotation belongs to Operation + case summaryAttr, routerAttr, successAttr, failureAttr, responseAttr: + return false } + // } return true } @@ -873,6 +955,7 @@ func (parser *Parser) matchTags(comments []*ast.Comment) (match bool) { if _, has := parser.tags["!"+tag]; has { return false } + if _, has := parser.tags[tag]; has { match = true // keep iterating as it may contain a tag that is excluded } @@ -880,25 +963,28 @@ func (parser *Parser) matchTags(comments []*ast.Comment) (match bool) { } return } + return true } func matchExtension(extensionToMatch string, comments []*ast.Comment) (match bool) { - if len(extensionToMatch) != 0 { - for _, comment := range comments { - commentLine := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) - fields := FieldsByAnySpace(commentLine, 2) - if len(fields) > 0 { - lowerAttribute := strings.ToLower(fields[0]) + if len(extensionToMatch) == 0 { + return true + } - if lowerAttribute == fmt.Sprintf("@x-%s", strings.ToLower(extensionToMatch)) { - return true - } + for _, comment := range comments { + commentLine := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) + fields := FieldsByAnySpace(commentLine, 2) + if len(fields) > 0 { + lowerAttribute := strings.ToLower(fields[0]) + + if lowerAttribute == fmt.Sprintf("@x-%s", strings.ToLower(extensionToMatch)) { + return true } } - return false } - return true + + return false } // ParseRouterAPIInfo parses router api info for given astFile. @@ -907,12 +993,14 @@ func (parser *Parser) ParseRouterAPIInfo(fileInfo *AstFileInfo) error { if (fileInfo.ParseFlag & ParseOperations) == ParseNone { continue } + 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 { @@ -923,6 +1011,7 @@ func (parser *Parser) ParseRouterAPIInfo(fileInfo *AstFileInfo) error { if err != nil { return err } + } } } @@ -1019,6 +1108,7 @@ func (parser *Parser) getTypeSchema(typeName string, file *ast.File, ref bool) ( typeSpecDef := parser.packages.FindTypeSpec(typeName, file) if typeSpecDef == nil { + parser.packages.FindTypeSpec(typeName, file) // uncomment for debugging return nil, fmt.Errorf("cannot find type definition: %s", typeName) } @@ -1543,6 +1633,8 @@ func defineTypeOfExample(schemaType, arrayType, exampleValue string) (interface{ } return result, nil + case ANY: + return exampleValue, nil } return nil, fmt.Errorf("%s is unsupported type in example value %s", schemaType, exampleValue) @@ -1689,4 +1781,10 @@ func (parser *Parser) addTestType(typename string) { Name: typename, Schema: PrimitiveSchema(OBJECT), } + + parser.parsedSchemasV3[typeDef] = &SchemaV3{ + PkgPath: "", + Name: typename, + Schema: PrimitiveSchemaV3(OBJECT).Spec, + } } diff --git a/parser_test.go b/parser_test.go index f070376db..ea29728bb 100644 --- a/parser_test.go +++ b/parser_test.go @@ -371,7 +371,7 @@ func TestParser_ParseGeneralApiInfoExtensions(t *testing.T) { t.Run("Test invalid extension value", func(t *testing.T) { t.Parallel() - expected := "annotation @x-google-endpoints need a valid json value" + expected := "annotation @x-google-endpoints need a valid json value. error: invalid character ':' after array element" gopath := os.Getenv("GOPATH") assert.NotNil(t, gopath) diff --git a/parserv3.go b/parserv3.go new file mode 100644 index 000000000..b0f49c5df --- /dev/null +++ b/parserv3.go @@ -0,0 +1,995 @@ +package swag + +import ( + "encoding/json" + "fmt" + "go/ast" + "go/token" + "net/http" + "reflect" + "sort" + "strings" + + "github.com/pkg/errors" + "github.com/sv-tools/openapi/spec" +) + +// FieldParserFactory create FieldParser. +type FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 + +// FieldParser parse struct field. +type FieldParserV3 interface { + ShouldSkip() bool + FieldName() (string, error) + FormName() string + CustomSchema() (*spec.RefOrSpec[spec.Schema], error) + ComplementSchema(schema *spec.RefOrSpec[spec.Schema]) error + IsRequired() (bool, error) +} + +// GetOpenAPI returns *spec.OpenAPI which is the root document object for the API specification. +func (parser *Parser) GetOpenAPI() *spec.OpenAPI { + return parser.openAPI +} + +func (p *Parser) parseGeneralAPIInfoV3(comments []string) error { + previousAttribute := "" + + // parsing classic meta data model + for line := 0; line < len(comments); line++ { + commentLine := comments[line] + commentLine = strings.TrimSpace(commentLine) + if len(commentLine) == 0 { + continue + } + fields := FieldsByAnySpace(commentLine, 2) + + attribute := fields[0] + var value string + if len(fields) > 1 { + value = fields[1] + } + + switch attr := strings.ToLower(attribute); attr { + case versionAttr, titleAttr, tosAttr, licNameAttr, licURLAttr, conNameAttr, conURLAttr, conEmailAttr: + setspecInfo(p.openAPI, attr, value) + case descriptionAttr: + if previousAttribute == attribute { + p.openAPI.Info.Spec.Description += "\n" + value + + continue + } + + setspecInfo(p.openAPI, attr, value) + case descriptionMarkdownAttr: + commentInfo, err := getMarkdownForTag("api", p.markdownFileDir) + if err != nil { + return err + } + + setspecInfo(p.openAPI, attr, string(commentInfo)) + case "@host": + if len(p.openAPI.Servers) == 0 { + server := spec.NewServer() + server.Spec.URL = value + p.openAPI.Servers = append(p.openAPI.Servers, server) + } + + println("@host is deprecated use servers instead") + case "@basepath": + if len(p.openAPI.Servers) == 0 { + server := spec.NewServer() + p.openAPI.Servers = append(p.openAPI.Servers, server) + } + p.openAPI.Servers[0].Spec.URL += value + + println("@basepath is deprecated use servers instead") + + case acceptAttr: + println("acceptAttribute is deprecated, as there is no such field on top level in spec V3.1") + case produceAttr: + println("produce is deprecated, as there is no such field on top level in spec V3.1") + case "@schemes": + println("@schemes is deprecated use servers instead") + case "@tag.name": + tag := &spec.Extendable[spec.Tag]{ + Spec: &spec.Tag{ + Name: value, + }, + } + + p.openAPI.Tags = append(p.openAPI.Tags, tag) + case "@tag.description": + tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] + tag.Spec.Description = value + case "@tag.description.markdown": + tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] + + commentInfo, err := getMarkdownForTag(tag.Spec.Name, p.markdownFileDir) + if err != nil { + return err + } + + tag.Spec.Description = string(commentInfo) + case "@tag.docs.url": + tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] + tag.Spec.ExternalDocs = spec.NewExternalDocs() + tag.Spec.ExternalDocs.Spec.URL = value + case "@tag.docs.description": + tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] + if tag.Spec.ExternalDocs == nil { + return fmt.Errorf("%s needs to come after a @tags.docs.url", attribute) + } + + tag.Spec.ExternalDocs.Spec.Description = value + case secBasicAttr, secAPIKeyAttr, secApplicationAttr, secImplicitAttr, secPasswordAttr, secAccessCodeAttr: + key, scheme, err := parseSecAttributesV3(attribute, comments, &line) + if err != nil { + return err + } + + schemeSpec := spec.NewSecuritySchemeSpec() + schemeSpec.Spec.Spec = scheme + + if p.openAPI.Components.Spec.SecuritySchemes == nil { + p.openAPI.Components.Spec.SecuritySchemes = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.SecurityScheme]]) + } + + p.openAPI.Components.Spec.SecuritySchemes[key] = schemeSpec + + case "@query.collection.format": + p.collectionFormatInQuery = TransToValidCollectionFormat(value) + + case extDocsDescAttr, extDocsURLAttr: + if p.openAPI.ExternalDocs == nil { + p.openAPI.ExternalDocs = spec.NewExternalDocs() + } + + switch attr { + case extDocsDescAttr: + p.openAPI.ExternalDocs.Spec.Description = value + case extDocsURLAttr: + p.openAPI.ExternalDocs.Spec.URL = value + } + + case "@x-taggroups": + originalAttribute := strings.Split(commentLine, " ")[0] + if len(value) == 0 { + return fmt.Errorf("annotation %s need a value", attribute) + } + + var valueJSON interface{} + if err := json.Unmarshal([]byte(value), &valueJSON); err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", originalAttribute, err.Error()) + } + + p.openAPI.Info.Extensions[originalAttribute[1:]] = valueJSON + default: + if strings.HasPrefix(attribute, "@x-") { + err := p.parseExtensionsV3(value, attribute) + if err != nil { + return errors.Wrap(err, "could not parse extension comment") + } + } + } + + previousAttribute = attribute + } + + return nil +} + +func (p *Parser) parseExtensionsV3(value, attribute string) error { + extensionName := attribute[1:] + + // // for each security definition + // for _, v := range p.openAPI.Components.Spec.SecuritySchemes{ + // // check if extension exists + // _, extExistsInSecurityDef := v.VendorExtensible.Extensions.GetString(extensionName) + // // if it exists in at least one, then we stop iterating + // if extExistsInSecurityDef { + // return nil + // } + // } + + if len(value) == 0 { + return fmt.Errorf("annotation %s need a value", attribute) + } + + if p.openAPI.Info.Extensions == nil { + p.openAPI.Info.Extensions = map[string]any{} + } + + var valueJSON interface{} + err := json.Unmarshal([]byte(value), &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) + } + + if strings.Contains(extensionName, "logo") { + p.openAPI.Info.Extensions[extensionName] = valueJSON + return nil + } + + p.openAPI.Info.Extensions[attribute[1:]] = valueJSON + + return nil +} + +func setspecInfo(openAPI *spec.OpenAPI, attribute, value string) { + switch attribute { + case versionAttr: + openAPI.Info.Spec.Version = value + case titleAttr: + openAPI.Info.Spec.Title = value + case tosAttr: + openAPI.Info.Spec.TermsOfService = value + case descriptionAttr: + openAPI.Info.Spec.Description = value + case conNameAttr: + if openAPI.Info.Spec.Contact == nil { + openAPI.Info.Spec.Contact = spec.NewContact() + } + + openAPI.Info.Spec.Contact.Spec.Name = value + case conEmailAttr: + if openAPI.Info.Spec.Contact == nil { + openAPI.Info.Spec.Contact = spec.NewContact() + } + + openAPI.Info.Spec.Contact.Spec.Email = value + case conURLAttr: + if openAPI.Info.Spec.Contact == nil { + openAPI.Info.Spec.Contact = spec.NewContact() + } + + openAPI.Info.Spec.Contact.Spec.URL = value + case licNameAttr: + if openAPI.Info.Spec.License == nil { + openAPI.Info.Spec.License = spec.NewLicense() + } + openAPI.Info.Spec.License.Spec.Name = value + case licURLAttr: + if openAPI.Info.Spec.License == nil { + openAPI.Info.Spec.License = spec.NewLicense() + } + openAPI.Info.Spec.License.Spec.URL = value + } +} + +func parseSecAttributesV3(context string, lines []string, index *int) (string, *spec.SecurityScheme, error) { + const ( + in = "@in" + name = "@name" + descriptionAttr = "@description" + tokenURL = "@tokenurl" + authorizationURL = "@authorizationurl" + ) + + var search []string + + attribute := strings.ToLower(FieldsByAnySpace(lines[*index], 2)[0]) + switch attribute { + case secBasicAttr: + scheme := spec.SecurityScheme{ + Type: "http", + Scheme: "basic", + } + return "basic", &scheme, nil + case secAPIKeyAttr: + search = []string{in, name} + case secApplicationAttr, secPasswordAttr: + search = []string{tokenURL, in, name} + case secImplicitAttr: + search = []string{authorizationURL, in} + case secAccessCodeAttr: + search = []string{tokenURL, authorizationURL, in} + } + + // For the first line we get the attributes in the context parameter, so we skip to the next one + *index++ + + attrMap, scopes := make(map[string]string), make(map[string]string) + extensions, description := make(map[string]interface{}), "" + + for ; *index < len(lines); *index++ { + v := strings.TrimSpace(lines[*index]) + if len(v) == 0 { + continue + } + + fields := FieldsByAnySpace(v, 2) + securityAttr := strings.ToLower(fields[0]) + var value string + if len(fields) > 1 { + value = fields[1] + } + + for _, findTerm := range search { + if securityAttr == findTerm { + attrMap[securityAttr] = value + + break + } + } + + isExists, err := isExistsScope(securityAttr) + if err != nil { + return "", nil, err + } + + if isExists { + scopes[securityAttr[len(scopeAttrPrefix):]] = v[len(securityAttr):] + } + + if strings.HasPrefix(securityAttr, "@x-") { + // Add the custom attribute without the @ + extensions[securityAttr[1:]] = value + } + + // Not mandatory field + if securityAttr == descriptionAttr { + description = value + } + + // next securityDefinitions + if strings.Index(securityAttr, "@securitydefinitions.") == 0 { + // Go back to the previous line and break + *index-- + + break + } + } + + if len(attrMap) != len(search) { + return "", nil, fmt.Errorf("%s is %v required", context, search) + } + + scheme := &spec.SecurityScheme{} + key := getSecurityDefinitionKey(lines) + + switch attribute { + case secAPIKeyAttr: + scheme.Type = "apiKey" + scheme.In = attrMap[in] + scheme.Name = attrMap[name] + case secApplicationAttr: + scheme.Type = "oauth2" + scheme.In = attrMap[in] + scheme.Flows = spec.NewOAuthFlows() + scheme.Flows.Spec.ClientCredentials = spec.NewOAuthFlow() + scheme.Flows.Spec.ClientCredentials.Spec.TokenURL = attrMap[tokenURL] + + scheme.Flows.Spec.ClientCredentials.Spec.Scopes = make(map[string]string) + for k, v := range scopes { + scheme.Flows.Spec.ClientCredentials.Spec.Scopes[k] = v + } + case secImplicitAttr: + scheme.Type = "oauth2" + scheme.In = attrMap[in] + scheme.Flows = spec.NewOAuthFlows() + scheme.Flows.Spec.Implicit = spec.NewOAuthFlow() + scheme.Flows.Spec.Implicit.Spec.AuthorizationURL = attrMap[authorizationURL] + scheme.Flows.Spec.Implicit.Spec.Scopes = make(map[string]string) + for k, v := range scopes { + scheme.Flows.Spec.Implicit.Spec.Scopes[k] = v + } + case secPasswordAttr: + scheme.Type = "oauth2" + scheme.In = attrMap[in] + scheme.Flows = spec.NewOAuthFlows() + scheme.Flows.Spec.Password = spec.NewOAuthFlow() + scheme.Flows.Spec.Password.Spec.TokenURL = attrMap[tokenURL] + + scheme.Flows.Spec.Password.Spec.Scopes = make(map[string]string) + for k, v := range scopes { + scheme.Flows.Spec.Password.Spec.Scopes[k] = v + } + + case secAccessCodeAttr: + scheme.Type = "oauth2" + scheme.In = attrMap[in] + scheme.Flows = spec.NewOAuthFlows() + scheme.Flows.Spec.AuthorizationCode = spec.NewOAuthFlow() + scheme.Flows.Spec.AuthorizationCode.Spec.AuthorizationURL = attrMap[authorizationURL] + scheme.Flows.Spec.AuthorizationCode.Spec.TokenURL = attrMap[tokenURL] + } + + scheme.Description = description + + if scheme.Flows != nil && scheme.Flows.Extensions == nil && len(extensions) > 0 { + scheme.Flows.Extensions = make(map[string]interface{}) + } + + for k, v := range extensions { + scheme.Flows.Extensions[k] = v + } + + return key, scheme, nil +} + +func getSecurityDefinitionKey(lines []string) string { + for _, line := range lines { + if strings.HasPrefix(strings.ToLower(line), "@securitydefinitions") { + splittedLine := strings.Split(line, " ") + return splittedLine[len(splittedLine)-1] + } + } + + return "" +} + +// ParseRouterAPIInfo parses router api info for given astFile. +func (parser *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { + for _, astDescription := range fileInfo.File.Decls { + if (fileInfo.ParseFlag & ParseOperations) == ParseNone { + continue + } + + astDeclaration, ok := astDescription.(*ast.FuncDecl) + if !ok || astDeclaration.Doc == nil || astDeclaration.Doc.List == nil { + continue + } + + if parser.matchTags(astDeclaration.Doc.List) && + matchExtension(parser.parseExtension, astDeclaration.Doc.List) { + // for per 'function' comment, create a new 'Operation' object + operation := NewOperationV3(parser, SetCodeExampleFilesDirectoryV3(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) + } + } + err := processRouterOperationV3(parser, operation) + if err != nil { + return err + } + } + } + + return nil +} + +func processRouterOperationV3(p *Parser, o *OperationV3) error { + for _, routeProperties := range o.RouterProperties { + var ( + pathItem *spec.RefOrSpec[spec.Extendable[spec.PathItem]] + ok bool + ) + + pathItem, ok = p.openAPI.Paths.Spec.Paths[routeProperties.Path] + if !ok { + pathItem = &spec.RefOrSpec[spec.Extendable[spec.PathItem]]{ + Spec: &spec.Extendable[spec.PathItem]{ + Spec: &spec.PathItem{}, + }, + } + } + + op := refRouteMethodOpV3(pathItem.Spec.Spec, routeProperties.HTTPMethod) + + // check if we already have an operation for this path and method + if *op != nil { + err := fmt.Errorf("route %s %s is declared multiple times", routeProperties.HTTPMethod, routeProperties.Path) + if p.Strict { + return err + } + + p.debug.Printf("warning: %s\n", err) + } + + *op = &o.Operation + + p.openAPI.Paths.Spec.Paths[routeProperties.Path] = pathItem + } + + return nil +} + +func refRouteMethodOpV3(item *spec.PathItem, method string) **spec.Operation { + switch method { + case http.MethodGet: + if item.Get == nil { + item.Get = &spec.Extendable[spec.Operation]{} + } + return &item.Get.Spec + case http.MethodPost: + if item.Post == nil { + item.Post = &spec.Extendable[spec.Operation]{} + } + return &item.Post.Spec + case http.MethodDelete: + if item.Delete == nil { + item.Delete = &spec.Extendable[spec.Operation]{} + } + return &item.Delete.Spec + case http.MethodPut: + if item.Put == nil { + item.Put = &spec.Extendable[spec.Operation]{} + } + return &item.Put.Spec + case http.MethodPatch: + if item.Patch == nil { + item.Patch = &spec.Extendable[spec.Operation]{} + } + return &item.Patch.Spec + case http.MethodHead: + if item.Head == nil { + item.Head = &spec.Extendable[spec.Operation]{} + } + return &item.Head.Spec + case http.MethodOptions: + if item.Options == nil { + item.Options = &spec.Extendable[spec.Operation]{} + } + return &item.Options.Spec + default: + return nil + } +} + +func (p *Parser) getTypeSchemaV3(typeName string, file *ast.File, ref bool) (*spec.RefOrSpec[spec.Schema], error) { + if override, ok := p.Overrides[typeName]; ok { + p.debug.Printf("Override detected for %s: using %s instead", typeName, override) + schema, err := parseObjectSchemaV3(p, override, file) + if err != nil { + return nil, err + } + + return schema, nil + + } + + if IsInterfaceLike(typeName) { + return spec.NewSchemaSpec(), nil + } + + if IsGolangPrimitiveType(typeName) { + return PrimitiveSchemaV3(TransToValidSchemeType(typeName)), nil + } + + schemaType, err := convertFromSpecificToPrimitive(typeName) + if err == nil { + return PrimitiveSchemaV3(schemaType), nil + } + + typeSpecDef := p.packages.FindTypeSpec(typeName, file) + if typeSpecDef == nil { + p.packages.FindTypeSpec(typeName, file) // uncomment for debugging + return nil, fmt.Errorf("cannot find type definition: %s", typeName) + } + + if override, ok := p.Overrides[typeSpecDef.FullPath()]; ok { + if override == "" { + p.debug.Printf("Override detected for %s: ignoring", typeSpecDef.FullPath()) + + return nil, ErrSkippedField + } + + p.debug.Printf("Override detected for %s: using %s instead", typeSpecDef.FullPath(), override) + + separator := strings.LastIndex(override, ".") + if separator == -1 { + // treat as a swaggertype tag + parts := strings.Split(override, ",") + return BuildCustomSchemaV3(parts) + } + + typeSpecDef = p.packages.findTypeSpec(override[0:separator], override[separator+1:]) + } + + schema, ok := p.parsedSchemasV3[typeSpecDef] + if !ok { + var err error + + schema, err = p.ParseDefinitionV3(typeSpecDef) + if err != nil { + if err == ErrRecursiveParseStruct && ref { + return p.getRefTypeSchemaV3(typeSpecDef, schema), nil + } + return nil, err + } + } + + if ref { + if IsComplexSchemaV3(schema) { + return p.getRefTypeSchemaV3(typeSpecDef, schema), nil + } + + // if it is a simple schema, just return a copy + newSchema := *schema.Schema + return spec.NewRefOrSpec(nil, &newSchema), nil + } + + return spec.NewRefOrSpec(nil, schema.Schema), nil +} + +// ParseDefinitionV3 parses given type spec that corresponds to the type under +// given name and package, and populates swagger schema definitions registry +// with a schema for the given type +func (p *Parser) ParseDefinitionV3(typeSpecDef *TypeSpecDef) (*SchemaV3, error) { + typeName := typeSpecDef.TypeName() + schema, found := p.parsedSchemasV3[typeSpecDef] + if found { + p.debug.Printf("Skipping '%s', already parsed.", typeName) + + return schema, nil + } + + if p.isInStructStack(typeSpecDef) { + p.debug.Printf("Skipping '%s', recursion detected.", typeName) + + return &SchemaV3{ + Name: typeName, + PkgPath: typeSpecDef.PkgPath, + Schema: PrimitiveSchemaV3(OBJECT).Spec, + }, + ErrRecursiveParseStruct + } + + p.structStack = append(p.structStack, typeSpecDef) + + p.debug.Printf("Generating %s", typeName) + + definition, err := p.parseTypeExprV3(typeSpecDef.File, typeSpecDef.TypeSpec.Type, false) + if err != nil { + p.debug.Printf("Error parsing type definition '%s': %s", typeName, err) + return nil, err + } + + if definition.Spec.Description == "" { + fillDefinitionDescriptionV3(definition.Spec, typeSpecDef.File, typeSpecDef) + } + + if len(typeSpecDef.Enums) > 0 { + var varNames []string + var enumComments = make(map[string]string) + for _, value := range typeSpecDef.Enums { + definition.Spec.Enum = append(definition.Spec.Enum, value.Value) + varNames = append(varNames, value.key) + if len(value.Comment) > 0 { + enumComments[value.key] = value.Comment + } + } + + if definition.Spec.Extensions == nil { + definition.Spec.Extensions = make(map[string]any) + } + + definition.Spec.Extensions[enumVarNamesExtension] = varNames + if len(enumComments) > 0 { + definition.Spec.Extensions[enumCommentsExtension] = enumComments + } + } + + sch := SchemaV3{ + Name: typeName, + PkgPath: typeSpecDef.PkgPath, + Schema: definition.Spec, + } + p.parsedSchemasV3[typeSpecDef] = &sch + + // update an empty schema as a result of recursion + s2, found := p.outputSchemasV3[typeSpecDef] + if found { + p.openAPI.Components.Spec.Schemas[s2.Name] = definition + } + + return &sch, nil +} + +// fillDefinitionDescription additionally fills fields in definition (spec.Schema) +// TODO: If .go file contains many types, it may work for a long time +func fillDefinitionDescriptionV3(definition *spec.Schema, file *ast.File, typeSpecDef *TypeSpecDef) { + for _, astDeclaration := range file.Decls { + generalDeclaration, ok := astDeclaration.(*ast.GenDecl) + if !ok || generalDeclaration.Tok != token.TYPE { + continue + } + + for _, astSpec := range generalDeclaration.Specs { + typeSpec, ok := astSpec.(*ast.TypeSpec) + if !ok || typeSpec != typeSpecDef.TypeSpec { + continue + } + + definition.Description = + extractDeclarationDescription(typeSpec.Doc, typeSpec.Comment, generalDeclaration.Doc) + } + } +} + +// parseTypeExprV3 parses given type expression that corresponds to the type under +// given name and package, and returns swagger schema for it. +func (p *Parser) parseTypeExprV3(file *ast.File, typeExpr ast.Expr, ref bool) (*spec.RefOrSpec[spec.Schema], error) { + const errMessage = "parse type expression v3" + + switch expr := typeExpr.(type) { + // type Foo interface{} + case *ast.InterfaceType: + return spec.NewSchemaSpec(), nil + + // type Foo struct {...} + case *ast.StructType: + return p.parseStructV3(file, expr.Fields) + + // type Foo Baz + case *ast.Ident: + result, err := p.getTypeSchemaV3(expr.Name, file, true) + if err != nil { + return nil, errors.Wrap(err, errMessage) + } + + return result, nil + // type Foo *Baz + case *ast.StarExpr: + return p.parseTypeExprV3(file, expr.X, ref) + + // type Foo pkg.Bar + case *ast.SelectorExpr: + if xIdent, ok := expr.X.(*ast.Ident); ok { + result, err := p.getTypeSchemaV3(fullTypeName(xIdent.Name, expr.Sel.Name), file, ref) + if err != nil { + return nil, errors.Wrap(err, errMessage) + } + + return result, nil + } + // type Foo []Baz + case *ast.ArrayType: + itemSchema, err := p.parseTypeExprV3(file, expr.Elt, true) + if err != nil { + return nil, err + } + + if itemSchema == nil { + schema := &spec.Schema{} + schema.Type = spec.NewSingleOrArray(ARRAY) + schema.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + p.debug.Printf("Creating array with empty item schema %v", expr.Elt) + + return spec.NewRefOrSpec(nil, schema), nil + } + + result := &spec.Schema{} + result.Type = spec.NewSingleOrArray(ARRAY) + result.Items = spec.NewBoolOrSchema(false, itemSchema) + + return spec.NewRefOrSpec(nil, result), nil + // type Foo map[string]Bar + case *ast.MapType: + if _, ok := expr.Value.(*ast.InterfaceType); ok { + result := &spec.Schema{} + result.AdditionalProperties = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + result.Type = spec.NewSingleOrArray(OBJECT) + + return spec.NewRefOrSpec(nil, result), nil + } + + schema, err := p.parseTypeExprV3(file, expr.Value, true) + if err != nil { + return nil, err + } + + result := &spec.Schema{} + result.AdditionalProperties = spec.NewBoolOrSchema(false, schema) + result.Type = spec.NewSingleOrArray(OBJECT) + + return spec.NewRefOrSpec(nil, result), nil + case *ast.FuncType: + return nil, ErrFuncTypeField + // ... + } + + return p.parseGenericTypeExprV3(file, typeExpr) +} + +func (p *Parser) parseStructV3(file *ast.File, fields *ast.FieldList) (*spec.RefOrSpec[spec.Schema], error) { + required, properties := make([]string, 0), make(map[string]*spec.RefOrSpec[spec.Schema]) + + for _, field := range fields.List { + fieldProps, requiredFromAnon, err := p.parseStructFieldV3(file, field) + if err != nil { + if err == ErrFuncTypeField || err == ErrSkippedField { + continue + } + + return nil, err + } + + if len(fieldProps) == 0 { + continue + } + + required = append(required, requiredFromAnon...) + + for k, v := range fieldProps { + properties[k] = v + } + } + + sort.Strings(required) + + result := spec.NewSchemaSpec() + result.Spec.Type = spec.NewSingleOrArray(OBJECT) + result.Spec.Properties = properties + result.Spec.Required = required + + return result, nil +} + +func (p *Parser) parseStructFieldV3(file *ast.File, field *ast.Field) (map[string]*spec.RefOrSpec[spec.Schema], []string, error) { + if field.Tag != nil { + skip, ok := reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", "")).Lookup("swaggerignore") + if ok && strings.EqualFold(skip, "true") { + return nil, nil, nil + } + } + + ps := p.fieldParserFactoryV3(p, field) + + if ps.ShouldSkip() { + return nil, nil, nil + } + + fieldName, err := ps.FieldName() + if err != nil { + return nil, nil, err + } + + if fieldName == "" { + typeName, err := getFieldType(file, field.Type, nil) + if err != nil { + return nil, nil, err + } + + schema, err := p.getTypeSchemaV3(typeName, file, false) + if err != nil { + return nil, nil, err + } + + if len(schema.Spec.Type) > 0 && schema.Spec.Type[0] == OBJECT { + if len(schema.Spec.Properties) == 0 { + return nil, nil, nil + } + + properties := make(map[string]*spec.RefOrSpec[spec.Schema]) + for k, v := range schema.Spec.Properties { + properties[k] = v + } + + return properties, schema.Spec.Required, nil + } + // for alias type of non-struct types ,such as array,map, etc. ignore field tag. + return map[string]*spec.RefOrSpec[spec.Schema]{ + typeName: schema, + }, nil, nil + + } + + schema, err := ps.CustomSchema() + if err != nil { + return nil, nil, err + } + + if schema == nil { + typeName, err := getFieldType(file, field.Type, nil) + if err == nil { + // named type + schema, err = p.getTypeSchemaV3(typeName, file, true) + if err != nil { + return nil, nil, err + } + + } else { + // unnamed type + parsedSchema, err := p.parseTypeExprV3(file, field.Type, false) + if err != nil { + return nil, nil, err + } + + schema = parsedSchema + } + } + + err = ps.ComplementSchema(schema) + if err != nil { + return nil, nil, err + } + + var tagRequired []string + + required, err := ps.IsRequired() + if err != nil { + return nil, nil, err + } + + if required { + tagRequired = append(tagRequired, fieldName) + } + + if formName := ps.FormName(); len(formName) > 0 { + if schema.Spec.Extensions == nil { + schema.Spec.Extensions = make(map[string]any) + } + schema.Spec.Extensions[formTag] = formName + } + + return map[string]*spec.RefOrSpec[spec.Schema]{fieldName: schema}, tagRequired, nil +} + +func (p *Parser) getRefTypeSchemaV3(typeSpecDef *TypeSpecDef, schema *SchemaV3) *spec.RefOrSpec[spec.Schema] { + _, ok := p.outputSchemasV3[typeSpecDef] + if !ok { + if p.openAPI.Components.Spec.Schemas == nil { + p.openAPI.Components.Spec.Schemas = make(map[string]*spec.RefOrSpec[spec.Schema]) + } + + p.openAPI.Components.Spec.Schemas[schema.Name] = spec.NewSchemaSpec() + + if schema.Schema != nil { + p.openAPI.Components.Spec.Schemas[schema.Name] = spec.NewRefOrSpec(nil, schema.Schema) + } + + p.outputSchemasV3[typeSpecDef] = schema + } + + refSchema := RefSchemaV3(schema.Name) + + return refSchema +} + +// GetSchemaTypePath get path of schema type. +func (parser *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], depth int) []string { + if schema == nil || depth == 0 { + return nil + } + + name := "" + if schema.Ref != nil { + name = schema.Ref.Ref + } + + if name != "" { + if pos := strings.LastIndexByte(name, '/'); pos >= 0 { + name = name[pos+1:] + if schema, ok := parser.openAPI.Components.Spec.Schemas[name]; ok { + return parser.GetSchemaTypePathV3(schema, depth) + } + } + + return nil + } + + if schema.Spec != nil && len(schema.Spec.Type) > 0 { + switch schema.Spec.Type[0] { + case ARRAY: + depth-- + + s := []string{schema.Spec.Type[0]} + + return append(s, parser.GetSchemaTypePathV3(schema.Spec.Items.Schema, depth)...) + case OBJECT: + if schema.Spec.AdditionalProperties != nil && schema.Spec.AdditionalProperties.Schema != nil { + // for map + depth-- + + s := []string{schema.Spec.Type[0]} + + return append(s, parser.GetSchemaTypePathV3(schema.Spec.AdditionalProperties.Schema, depth)...) + } + } + + return []string{schema.Spec.Type[0]} + } + + println("found schema with no Type, returning any") + return []string{ANY} +} + +func (p *Parser) getSchemaByRef(ref *spec.Ref) *spec.Schema { + searchString := strings.ReplaceAll(ref.Ref, "#/components/schemas/", "") + return p.openAPI.Components.Spec.Schemas[searchString].Spec +} diff --git a/parserv3_test.go b/parserv3_test.go new file mode 100644 index 000000000..990b656d4 --- /dev/null +++ b/parserv3_test.go @@ -0,0 +1,369 @@ +package swag + +import ( + "go/ast" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOverridesGetTypeSchemaV3(t *testing.T) { + t.Parallel() + + overrides := map[string]string{ + "sql.NullString": "string", + } + + p := New(SetOverrides(overrides)) + + t.Run("Override sql.NullString by string", func(t *testing.T) { + t.Parallel() + + s, err := p.getTypeSchemaV3("sql.NullString", nil, false) + if assert.NoError(t, err) { + assert.Truef(t, s.Spec.Type[0] == "string", "type sql.NullString should be overridden by string") + } + }) + + t.Run("Missing Override for sql.NullInt64", func(t *testing.T) { + t.Parallel() + + _, err := p.getTypeSchemaV3("sql.NullInt64", nil, false) + if assert.Error(t, err) { + assert.Equal(t, "cannot find type definition: sql.NullInt64", err.Error()) + } + }) +} + +func TestParserParseDefinitionV3(t *testing.T) { + p := New() + + // Parsing existing type + definition := &TypeSpecDef{ + PkgPath: "github.com/swagger/swag", + File: &ast.File{ + Name: &ast.Ident{ + Name: "swag", + }, + }, + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{ + Name: "Test", + }, + }, + } + + expected := &SchemaV3{} + p.parsedSchemasV3[definition] = expected + + schema, err := p.ParseDefinitionV3(definition) + assert.NoError(t, err) + assert.Equal(t, expected, schema) + + // Parsing *ast.FuncType + 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{}, + }, + } + _, err = p.ParseDefinitionV3(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.ParseDefinitionV3(definition) + assert.Error(t, err) + assert.Equal(t, "model.TestFuncDecl.Test", definition.TypeName()) +} + +func TestParserParseGeneralApiInfoV3(t *testing.T) { + t.Parallel() + + gopath := os.Getenv("GOPATH") + assert.NotNil(t, gopath) + + p := New(SetOpenAPIVersion(true)) + + err := p.ParseGeneralAPIInfo("testdata/v3/main.go") + assert.NoError(t, err) + + assert.Equal(t, "This is a sample server Petstore server.\nIt has a lot of beautiful features.", p.openAPI.Info.Spec.Description) + assert.Equal(t, "Swagger Example API", p.openAPI.Info.Spec.Title) + assert.Equal(t, "http://swagger.io/terms/", p.openAPI.Info.Spec.TermsOfService) + assert.Equal(t, "API Support", p.openAPI.Info.Spec.Contact.Spec.Name) + assert.Equal(t, "http://www.swagger.io/support", p.openAPI.Info.Spec.Contact.Spec.URL) + assert.Equal(t, "support@swagger.io", p.openAPI.Info.Spec.Contact.Spec.Email) + assert.Equal(t, "Apache 2.0", p.openAPI.Info.Spec.License.Spec.Name) + assert.Equal(t, "http://www.apache.org/licenses/LICENSE-2.0.html", p.openAPI.Info.Spec.License.Spec.URL) + assert.Equal(t, "1.0", p.openAPI.Info.Spec.Version) + + xLogo := map[string]interface{}(map[string]interface{}{"altText": "Petstore logo", "backgroundColor": "#FFFFFF", "url": "https://redocly.github.io/redoc/petstore-logo.png"}) + assert.Equal(t, xLogo, p.openAPI.Info.Extensions["x-logo"]) + assert.Equal(t, "marks values", p.openAPI.Info.Extensions["x-google-marks"]) + + endpoints := interface{}([]interface{}{map[string]interface{}{"allowCors": true, "name": "name.endpoints.environment.cloud.goog"}}) + assert.Equal(t, endpoints, p.openAPI.Info.Extensions["x-google-endpoints"]) + + assert.Equal(t, "OpenAPI", p.openAPI.ExternalDocs.Spec.Description) + assert.Equal(t, "https://swagger.io/resources/open-api", p.openAPI.ExternalDocs.Spec.URL) + + assert.Equal(t, 6, len(p.openAPI.Components.Spec.SecuritySchemes)) + + security := p.openAPI.Components.Spec.SecuritySchemes + assert.Equal(t, "basic", security["basic"].Spec.Spec.Scheme) + assert.Equal(t, "http", security["basic"].Spec.Spec.Type) + + assert.Equal(t, "apiKey", security["ApiKeyAuth"].Spec.Spec.Type) + assert.Equal(t, "Authorization", security["ApiKeyAuth"].Spec.Spec.Name) + assert.Equal(t, "header", security["ApiKeyAuth"].Spec.Spec.In) + assert.Equal(t, "some description", security["ApiKeyAuth"].Spec.Spec.Description) + + assert.Equal(t, "oauth2", security["OAuth2Application"].Spec.Spec.Type) + assert.Equal(t, "header", security["OAuth2Application"].Spec.Spec.In) + assert.Equal(t, "https://example.com/oauth/token", security["OAuth2Application"].Spec.Spec.Flows.Spec.ClientCredentials.Spec.TokenURL) + assert.Equal(t, 2, len(security["OAuth2Application"].Spec.Spec.Flows.Spec.ClientCredentials.Spec.Scopes)) + + assert.Equal(t, "oauth2", security["OAuth2Implicit"].Spec.Spec.Type) + assert.Equal(t, "header", security["OAuth2Implicit"].Spec.Spec.In) + assert.Equal(t, "https://example.com/oauth/authorize", security["OAuth2Implicit"].Spec.Spec.Flows.Spec.Implicit.Spec.AuthorizationURL) + assert.Equal(t, "some_audience.google.com", security["OAuth2Implicit"].Spec.Spec.Flows.Extensions["x-google-audiences"]) + + assert.Equal(t, "oauth2", security["OAuth2Password"].Spec.Spec.Type) + assert.Equal(t, "header", security["OAuth2Password"].Spec.Spec.In) + assert.Equal(t, "https://example.com/oauth/token", security["OAuth2Password"].Spec.Spec.Flows.Spec.Password.Spec.TokenURL) + + assert.Equal(t, "oauth2", security["OAuth2AccessCode"].Spec.Spec.Type) + assert.Equal(t, "header", security["OAuth2AccessCode"].Spec.Spec.In) + assert.Equal(t, "https://example.com/oauth/token", security["OAuth2AccessCode"].Spec.Spec.Flows.Spec.AuthorizationCode.Spec.TokenURL) +} + +func TestParser_ParseGeneralApiInfoExtensionsV3(t *testing.T) { + // should return an error because extension value is not a valid json + t.Run("Test invalid extension value", func(t *testing.T) { + t.Parallel() + + expected := "could not parse extension comment: annotation @x-google-endpoints need a valid json value. error: invalid character ':' after array element" + gopath := os.Getenv("GOPATH") + assert.NotNil(t, gopath) + + p := New(SetOpenAPIVersion(true)) + + err := p.ParseGeneralAPIInfo("testdata/v3/extensionsFail1.go") + if assert.Error(t, err) { + assert.Equal(t, expected, err.Error()) + } + }) + + // should return an error because extension don't have a value + t.Run("Test missing extension value", func(t *testing.T) { + t.Parallel() + + expected := "could not parse extension comment: annotation @x-google-endpoints need a value" + gopath := os.Getenv("GOPATH") + assert.NotNil(t, gopath) + + p := New(SetOpenAPIVersion(true)) + + err := p.ParseGeneralAPIInfo("testdata/v3/extensionsFail2.go") + if assert.Error(t, err) { + assert.Equal(t, expected, err.Error()) + } + }) +} + +func TestParserParseGeneralApiInfoWithOpsInSameFileV3(t *testing.T) { + t.Parallel() + + gopath := os.Getenv("GOPATH") + assert.NotNil(t, gopath) + + p := New(SetOpenAPIVersion(true)) + + err := p.ParseGeneralAPIInfo("testdata/single_file_api/main.go") + assert.NoError(t, err) + + assert.Equal(t, "This is a sample server Petstore server.\nIt has a lot of beautiful features.", p.openAPI.Info.Spec.Description) + assert.Equal(t, "Swagger Example API", p.openAPI.Info.Spec.Title) + assert.Equal(t, "http://swagger.io/terms/", p.openAPI.Info.Spec.TermsOfService) +} + +func TestParserParseGeneralAPIInfoMarkdownV3(t *testing.T) { + t.Parallel() + + p := New(SetMarkdownFileDirectory("testdata"), SetOpenAPIVersion(true)) + mainAPIFile := "testdata/markdown.go" + err := p.ParseGeneralAPIInfo(mainAPIFile) + assert.NoError(t, err) + + assert.Equal(t, "users", p.openAPI.Tags[0].Spec.Name) + assert.Equal(t, "Users Tag Markdown Description", p.openAPI.Tags[0].Spec.Description) + + p = New(SetOpenAPIVersion(true)) + + err = p.ParseGeneralAPIInfo(mainAPIFile) + assert.Error(t, err) +} + +func TestParserParseGeneralApiInfoFailedV3(t *testing.T) { + t.Parallel() + + gopath := os.Getenv("GOPATH") + assert.NotNil(t, gopath) + p := New(SetOpenAPIVersion(true)) + assert.Error(t, p.ParseGeneralAPIInfo("testdata/noexist.go")) +} + +func TestParserParseGeneralAPIInfoCollectionFormatV3(t *testing.T) { + t.Parallel() + + parser := New(SetOpenAPIVersion(true)) + assert.NoError(t, parser.parseGeneralAPIInfoV3([]string{ + "@query.collection.format csv", + })) + assert.Equal(t, parser.collectionFormatInQuery, "csv") + + assert.NoError(t, parser.parseGeneralAPIInfoV3([]string{ + "@query.collection.format tsv", + })) + assert.Equal(t, parser.collectionFormatInQuery, "tsv") +} + +func TestParserParseGeneralAPITagGroupsV3(t *testing.T) { + t.Parallel() + + parser := New(SetOpenAPIVersion(true)) + assert.NoError(t, parser.parseGeneralAPIInfoV3([]string{ + "@x-tagGroups [{\"name\":\"General\",\"tags\":[\"lanes\",\"video-recommendations\"]}]", + })) + + expected := []interface{}{map[string]interface{}{"name": "General", "tags": []interface{}{"lanes", "video-recommendations"}}} + assert.Equal(t, expected, parser.openAPI.Info.Extensions["x-tagGroups"]) +} + +func TestParserParseGeneralAPITagDocsV3(t *testing.T) { + t.Parallel() + + parser := New(SetOpenAPIVersion(true)) + assert.Error(t, parser.parseGeneralAPIInfoV3([]string{ + "@tag.name Test", + "@tag.docs.description Best example documentation"})) + + parser = New(SetOpenAPIVersion(true)) + err := parser.parseGeneralAPIInfoV3([]string{ + "@tag.name test", + "@tag.description A test Tag", + "@tag.docs.url https://example.com", + "@tag.docs.description Best example documentation"}) + assert.NoError(t, err) + + assert.Equal(t, "test", parser.openAPI.Tags[0].Spec.Name) + assert.Equal(t, "A test Tag", parser.openAPI.Tags[0].Spec.Description) + assert.Equal(t, "https://example.com", parser.openAPI.Tags[0].Spec.ExternalDocs.Spec.URL) + assert.Equal(t, "Best example documentation", parser.openAPI.Tags[0].Spec.ExternalDocs.Spec.Description) +} + +func TestGetAllGoFileInfoV3(t *testing.T) { + t.Parallel() + + searchDir := "testdata/pet" + + p := New(SetOpenAPIVersion(true)) + err := p.getAllGoFileInfo("testdata", searchDir) + + assert.NoError(t, err) + assert.Equal(t, 2, len(p.packages.files)) +} + +func TestParser_ParseTypeV3(t *testing.T) { + t.Parallel() + + searchDir := "testdata/v3/simple/" + + p := New(SetOpenAPIVersion(true)) + err := p.getAllGoFileInfo("testdata", searchDir) + assert.NoError(t, err) + + _, err = p.packages.ParseTypes() + + assert.NoError(t, err) + assert.NotNil(t, p.packages.uniqueDefinitions["api.Pet3"]) + assert.NotNil(t, p.packages.uniqueDefinitions["web.Pet"]) + assert.NotNil(t, p.packages.uniqueDefinitions["web.Pet2"]) +} + +func TestParsePet(t *testing.T) { + t.Parallel() + + searchDir := "testdata/v3/pet" + + p := New(SetOpenAPIVersion(true)) + p.PropNamingStrategy = PascalCase + + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + + schemas := p.openAPI.Components.Spec.Schemas + assert.NotNil(t, schemas) + + tagSchema := schemas["web.Tag"].Spec + assert.Equal(t, 2, len(tagSchema.Properties)) + assert.Equal(t, typeInteger, tagSchema.Properties["id"].Spec.Type) + assert.Equal(t, typeString, tagSchema.Properties["name"].Spec.Type) + + petSchema := schemas["web.Pet"].Spec + assert.NotNil(t, petSchema) + assert.Equal(t, 8, len(petSchema.Properties)) + assert.Equal(t, typeInteger, petSchema.Properties["id"].Spec.Type) + assert.Equal(t, typeString, petSchema.Properties["name"].Spec.Type) + +} + +func TestParseSimpleApiV3(t *testing.T) { + t.Parallel() + + searchDir := "testdata/v3/simple" + p := New(SetOpenAPIVersion(true)) + p.PropNamingStrategy = PascalCase + + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + + paths := p.openAPI.Paths.Spec.Paths + assert.Equal(t, 14, len(paths)) + + path := paths["/testapi/get-string-by-int/{some_id}"].Spec.Spec.Get.Spec + assert.Equal(t, "get string by ID", path.Description) + assert.Equal(t, "Add a new pet to the store", path.Summary) + assert.Equal(t, "get-string-by-int", path.OperationID) + + response := path.Responses.Spec.Response["200"] + assert.Equal(t, "ok", response.Spec.Spec.Description) + + //TODO add asserts +} diff --git a/schemav3.go b/schemav3.go new file mode 100644 index 000000000..f00a2df93 --- /dev/null +++ b/schemav3.go @@ -0,0 +1,141 @@ +package swag + +import ( + "errors" + + "github.com/sv-tools/openapi/spec" +) + +// PrimitiveSchemaV3 build a primitive schema. +func PrimitiveSchemaV3(refType string) *spec.RefOrSpec[spec.Schema] { + result := spec.NewSchemaSpec() + result.Spec.Type = spec.SingleOrArray[string]{refType} + + return result +} + +// IsComplexSchemaV3 whether a schema is complex and should be a ref schema +func IsComplexSchemaV3(schema *SchemaV3) bool { + // a enum type should be complex + if len(schema.Enum) > 0 { + return true + } + + // a deep array type is complex, how to determine deep? here more than 2 ,for example: [][]object,[][][]int + if len(schema.Type) > 2 { + return true + } + + //Object included, such as Object or []Object + for _, st := range schema.Type { + if st == OBJECT { + return true + } + } + return false +} + +// RefSchemaV3 build a reference schema. +func RefSchemaV3(refType string) *spec.RefOrSpec[spec.Schema] { + return spec.NewRefOrSpec[spec.Schema](spec.NewRef("#/components/schemas/"+refType), nil) +} + +// BuildCustomSchemaV3 build custom schema specified by tag swaggertype. +func BuildCustomSchemaV3(types []string) (*spec.RefOrSpec[spec.Schema], error) { + if len(types) == 0 { + return nil, nil + } + + switch types[0] { + case PRIMITIVE: + if len(types) == 1 { + return nil, errors.New("need primitive type after primitive") + } + + return BuildCustomSchemaV3(types[1:]) + case ARRAY: + if len(types) == 1 { + return nil, errors.New("need array item type after array") + } + + schema, err := BuildCustomSchemaV3(types[1:]) + if err != nil { + return nil, err + } + + // TODO: check if this is correct + result := spec.NewSchemaSpec() + result.Spec.Type = []string{"array"} + result.Spec.AdditionalProperties = spec.NewBoolOrSchema(true, schema) + + return result, nil + case OBJECT: + if len(types) == 1 { + return PrimitiveSchemaV3(types[0]), nil + } + + schema, err := BuildCustomSchemaV3(types[1:]) + if err != nil { + return nil, err + } + + result := spec.NewSchemaSpec() + result.Spec.AdditionalProperties = spec.NewBoolOrSchema(true, schema) + result.Spec.Type = spec.NewSingleOrArray("object") + + return result, nil + default: + err := CheckSchemaType(types[0]) + if err != nil { + return nil, err + } + + return PrimitiveSchemaV3(types[0]), nil + } +} + +// TransToValidCollectionFormatV3 determine valid collection format. +func TransToValidCollectionFormatV3(format, in string) string { + switch in { + case "query": + switch format { + case "form", "spaceDelimited", "pipeDelimited", "deepObject": + return format + case "ssv": + return "spaceDelimited" + case "pipes": + return "pipe" + case "multi": + return "form" + case "csv": + return "form" + default: + return "" + } + case "path": + switch format { + case "matrix", "label", "simple": + return format + case "csv": + return "simple" + default: + return "" + } + case "header": + switch format { + case "form", "simple": + return format + case "csv": + return "simple" + default: + return "" + } + case "cookie": + switch format { + case "form": + return format + } + } + + return "" +} diff --git a/testdata/code_examples/example.json b/testdata/code_examples/example.json index 26e1cef56..a093daa66 100644 --- a/testdata/code_examples/example.json +++ b/testdata/code_examples/example.json @@ -1,4 +1,6 @@ -{ - "lang": "JavaScript", - "source": "console.log('Hello World');" -} \ No newline at end of file +[ + { + "lang": "JavaScript", + "source": "console.log('Hello World');" + } +] diff --git a/testdata/generics_property/api/api.go b/testdata/generics_property/api/api.go index e68938f81..2ef9198c1 100644 --- a/testdata/generics_property/api/api.go +++ b/testdata/generics_property/api/api.go @@ -1,9 +1,10 @@ package api import ( + "net/http" + "github.com/swaggo/swag/testdata/generics_property/types" "github.com/swaggo/swag/testdata/generics_property/web" - "net/http" ) type NestedResponse struct { diff --git a/testdata/v3/extensionsFail1.go b/testdata/v3/extensionsFail1.go new file mode 100644 index 000000000..59a0cf989 --- /dev/null +++ b/testdata/v3/extensionsFail1.go @@ -0,0 +1,9 @@ +package main + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @description It has a lot of beautiful features. +// @termsOfService http://swagger.io/terms/ + +// @x-google-endpoints ["name":"name.endpoints.environment.cloud.goog","allowCors":true}] diff --git a/testdata/v3/extensionsFail2.go b/testdata/v3/extensionsFail2.go new file mode 100644 index 000000000..5618b19fd --- /dev/null +++ b/testdata/v3/extensionsFail2.go @@ -0,0 +1,9 @@ +package main + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @description It has a lot of beautiful features. +// @termsOfService http://swagger.io/terms/ + +// @x-google-endpoints diff --git a/testdata/v3/main.go b/testdata/v3/main.go new file mode 100644 index 000000000..de93578c1 --- /dev/null +++ b/testdata/v3/main.go @@ -0,0 +1,64 @@ +package main + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @description It has a lot of beautiful features. +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host petstore.swagger.io +// @BasePath /v2 +// @schemes http https +// @securityDefinitions.basic BasicAuth + +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name Authorization +// @description some description + +// @securitydefinitions.oauth2.application OAuth2Application +// @tokenUrl https://example.com/oauth/token +// @in header +// @name name +// @scope.write Grants write access +// @scope.admin Grants read and write access to administrative information + +// @securitydefinitions.oauth2.implicit OAuth2Implicit +// @authorizationurl https://example.com/oauth/authorize +// @in header +// @scope.write Grant +// @name names write access +// @scope.admin Grants read and write access to administrative information +// @x-google-audiences some_audience.google.com + +// @securitydefinitions.oauth2.password OAuth2Password +// @tokenUrl https://example.com/oauth/token +// @in header +// @name name +// @scope.read Grants read access +// @scope.write Grants write access +// @scope.admin Grants read and write access to administrative information + +// @securitydefinitions.oauth2.accessCode OAuth2AccessCode +// @tokenUrl https://example.com/oauth/token +// @authorizationurl https://example.com/oauth/authorize +// @scope.admin Grants read and write access to administrative information +// @x-tokenname id_token +// @in header +// @name name + +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api + +// @x-google-endpoints [{"name":"name.endpoints.environment.cloud.goog","allowCors":true}] +// @x-google-marks "marks values" +// @x-logo {"url":"https://redocly.github.io/redoc/petstore-logo.png", "altText": "Petstore logo", "backgroundColor": "#FFFFFF"} + +func main() {} diff --git a/testdata/v3/pet/main.go b/testdata/v3/pet/main.go new file mode 100644 index 000000000..90182a2e6 --- /dev/null +++ b/testdata/v3/pet/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/v3/pet/web" +) + +// @title Swagger Petstore +// @version 1.0 +// @description This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key 'special-key' to test the authorization filters. +// @termsOfService http://swagger.io/terms/ + +// @contact.email apiteam@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +func main() { + http.HandleFunc("/testapi/pets", web.GetPets) +} diff --git a/testdata/v3/pet/web/handler.go b/testdata/v3/pet/web/handler.go new file mode 100644 index 000000000..0945e71d2 --- /dev/null +++ b/testdata/v3/pet/web/handler.go @@ -0,0 +1,38 @@ +package web + +import "net/http" + +type Tag struct { + ID int `json:"id"` + Name string `json:"name"` +} + +type Pet struct { + ID int `json:"id" example:"1"` + Category struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"category_name"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"` + SmallCategory struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"detail_category_name"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"` + } `json:"small_category"` + } `json:"category"` + Name string `json:"name" example:"poti"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"` + Tags []Tag `json:"tags"` + Status string `json:"status"` + Price float32 `json:"price" example:"3.25"` + IsAlive bool `json:"is_alive" example:"true"` +} + +// @Summary Get all pets +// @Description get all pets +// @ID get-pets +// @Success 200 {object} []web.Pet "ok" +// @Router /testapi/pets [get] +func GetPets(w http.ResponseWriter, r *http.Request) { + _ = Cross{} + //write your code +} diff --git a/testdata/v3/simple/api/api.go b/testdata/v3/simple/api/api.go new file mode 100644 index 000000000..324385e58 --- /dev/null +++ b/testdata/v3/simple/api/api.go @@ -0,0 +1,140 @@ +package api + +import ( + "net/http" + + . "github.com/swaggo/swag/testdata/v3/simple/cross" + _ "github.com/swaggo/swag/testdata/v3/simple/web" +) + +// @Summary Add a new pet to the store +// @Description get string by ID +// @ID get-string-by-int +// @Accept json +// @Produce json +// @Param some_id path int true "Some ID" Format(int64) +// @Param some_id body web.Pet true "Some ID" +// @Success 200 {string} string "ok" +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 404 {object} web.APIError "Can not find ID" +// @Router /testapi/get-string-by-int/{some_id} [get] +func GetStringByInt(w http.ResponseWriter, r *http.Request) { + _ = Cross{} + //write your code +} + +// @Description get struct array by ID +// @ID get-struct-array-by-string +// @Accept json +// @Produce json +// @Param some_id path string true "Some ID" +// @Param category query int true "Category" Enums(1, 2, 3) +// @Param offset query int true "Offset" Minimum(0) default(0) +// @Param limit query int true "Limit" Maximum(50) default(10) +// @Param q query string true "q" Minlength(1) Maxlength(50) default("") +// @Success 200 {string} string "ok" +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 404 {object} web.APIError "Can not find ID" +// @Security ApiKeyAuth +// @Security BasicAuth +// @Security OAuth2Application[write] +// @Security OAuth2Implicit[read, admin] +// @Security OAuth2AccessCode[read] +// @Security OAuth2Password[admin] +// @Security OAuth2Implicit[read, write] || Firebase +// @Router /testapi/get-struct-array-by-string/{some_id} [get] +func GetStructArrayByString(w http.ResponseWriter, r *http.Request) { + //write your code +} + +// @Summary Upload file +// @Description Upload file +// @ID file.upload +// @Accept multipart/form-data +// @Produce json +// @Param file formData file true "this is a test file" +// @Success 200 {string} string "ok" +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 401 {array} string +// @Failure 404 {object} web.APIError "Can not find ID" +// @Failure 403 {object} Cross "cross" +// @Router /file/upload [post] +func Upload(w http.ResponseWriter, r *http.Request) { + //write your code +} + +// @Summary use Anonymous field +// @Success 200 {object} web.RevValue "ok" +// @Router /AnonymousField [get] +func AnonymousField() { + +} + +// @Summary use pet2 +// @Success 200 {object} web.Pet2 "ok" +// @Router /Pet2 [get] +func Pet2() { + +} + +// @Summary Use IndirectRecursiveTest +// @Success 200 {object} web.IndirectRecursiveTest +// @Router /IndirectRecursiveTest [get] +func IndirectRecursiveTest() { +} + +// @Summary Use Tags +// @Success 200 {object} web.Tags +// @Router /Tags [get] +func Tags() { +} + +// @Summary Use CrossAlias +// @Success 200 {object} web.CrossAlias +// @Router /CrossAlias [get] +func CrossAlias() { +} + +// @Summary Use AnonymousStructArray +// @Success 200 {object} web.AnonymousStructArray +// @Router /AnonymousStructArray [get] +func AnonymousStructArray() { +} + +type Pet3 struct { + ID int `json:"id"` +} + +// @Success 200 {object} web.Pet5a "ok" +// @Router /GetPet5a [options] +func GetPet5a() { + +} + +// @Success 200 {object} web.Pet5b "ok" +// @Router /GetPet5b [head] +func GetPet5b() { + +} + +// @Success 200 {object} web.Pet5c "ok" +// @Router /GetPet5c [patch] +func GetPet5c() { + +} + +type SwagReturn []map[string]string + +// @Success 200 {object} api.SwagReturn "ok" +// @Router /GetPet6MapString [get] +func GetPet6MapString() { + +} + +// @Success 200 {object} api.GetPet6FunctionScopedResponse.response "ok" +// @Router /GetPet6FunctionScopedResponse [get] +func GetPet6FunctionScopedResponse() { + type response struct { + Name string + } +} diff --git a/testdata/v3/simple/cross/test.go b/testdata/v3/simple/cross/test.go new file mode 100644 index 000000000..540e53f4f --- /dev/null +++ b/testdata/v3/simple/cross/test.go @@ -0,0 +1,6 @@ +package cross + +type Cross struct { + Array []string + String string +} diff --git a/testdata/v3/simple/main.go b/testdata/v3/simple/main.go new file mode 100644 index 000000000..a7cd1447a --- /dev/null +++ b/testdata/v3/simple/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/v3/simple/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host petstore.swagger.io +// @BasePath /v2 +func main() { + http.HandleFunc("/testapi/get-string-by-int/", api.GetStringByInt) + http.HandleFunc("/testapi/get-struct-array-by-string/", api.GetStructArrayByString) + http.HandleFunc("/testapi/upload", api.Upload) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/v3/simple/web/handler.go b/testdata/v3/simple/web/handler.go new file mode 100644 index 000000000..546fe322c --- /dev/null +++ b/testdata/v3/simple/web/handler.go @@ -0,0 +1,101 @@ +package web + +import ( + "time" + + "github.com/gofrs/uuid" + "github.com/shopspring/decimal" + "github.com/swaggo/swag/testdata/v3/simple/cross" +) + +type Pet struct { + ID int `json:"id" example:"1" format:"int64" readonly:"true"` + Category struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"category_name"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg" format:"url"` + SmallCategory struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"detail_category_name" binding:"required" minLength:"4" maxLength:"16"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"` + } `json:"small_category"` + } `json:"category"` + Name string `json:"name" example:"poti" binding:"required"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg" binding:"required"` + Tags []Tag `json:"tags"` + Pets *[]Pet2 `json:"pets"` + Pets2 []*Pet2 `json:"pets2"` + Status string `json:"status" enums:"healthy,ill"` + Price float32 `json:"price" example:"3.25" minimum:"1.0" maximum:"1000" multipleOf:"0.01"` + IsAlive bool `json:"is_alive" example:"true" default:"true"` + Data interface{} `json:"data"` + Hidden string `json:"-"` + UUID uuid.UUID `json:"uuid"` + Decimal decimal.Decimal `json:"decimal"` + IntArray []int `json:"int_array" example:"1,2"` + StringMap map[string]string `json:"string_map" example:"key1:value,key2:value2"` + EnumArray []int `json:"enum_array" enums:"1,2,3,5,7"` + FoodTypes []string `json:"food_types" swaggertype:"array,integer" enums:"0,1,2" x-enum-varnames:"Wet,Dry,Raw" extensions:"x-some-extension"` + FoodBrands []string `json:"food_brands" extensions:"x-some-extension"` + SingleEnumVarname string `json:"single_enum_varname" swaggertype:"integer" enums:"1,2,3" x-enum-varnames:"one,two,three" extensions:"x-some-extension"` +} + +type Tag struct { + ID int `json:"id" format:"int64"` + Name string `json:"name"` + Pets []Pet `json:"pets"` +} + +type Tags []*Tag + +type AnonymousStructArray []struct { + Foo string `json:"foo"` +} + +type CrossAlias cross.Cross + +type Pet2 struct { + ID int `json:"id"` + MiddleName *string `json:"middlename" extensions:"x-nullable,x-abc=def,!x-omitempty"` + DeletedAt *time.Time `json:"deleted_at"` +} + +type IndirectRecursiveTest struct { + Tags []Tag +} + +type APIError struct { + ErrorCode int + ErrorMessage string + CreatedAt time.Time +} + +type RevValueBase struct { + Status bool `json:"Status"` + + Err int32 `json:"Err,omitempty"` +} +type RevValue struct { + RevValueBase `json:"rev_value_base"` + + Data int `json:"Data"` + Cross cross.Cross `json:"cross"` + Crosses []cross.Cross `json:"crosses"` +} + +// Below we have Pet5b as base type and Pet5a and Pet5c both have Pet5b as anonymous field, inheriting it's properties +// By using these names we ensure that our test will fill if the order of parsing matters at all + +type Pet5a struct { + *Pet5b + Odd bool `json:"odd" binding:"required"` +} + +type Pet5b struct { + Name string `json:"name" binding:"required"` +} + +type Pet5c struct { + *Pet5b + Odd bool `json:"odd" binding:"required"` +} diff --git a/typesv3.go b/typesv3.go new file mode 100644 index 000000000..a7143bcc1 --- /dev/null +++ b/typesv3.go @@ -0,0 +1,10 @@ +package swag + +import "github.com/sv-tools/openapi/spec" + +// SchemaV3 parsed schema. +type SchemaV3 struct { + *spec.Schema // + PkgPath string // package import path used to rename Name of a definition int case of conflict + Name string // Name in definitions +} diff --git a/version.go b/version.go index 9abfa11b4..d91efdbb7 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package swag // Version of swag. -const Version = "v1.8.11" +const Version = "v2.0.0" From b060856c76f1db2cf3dfcccdec13e2e7cb390ecc Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Wed, 5 Apr 2023 22:05:54 +0300 Subject: [PATCH 02/34] chore: add v2 to ci (#1532) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e129e778..d16a00809 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: build on: push: - branches: [ master ] + branches: [ master, v2 ] pull_request: - branches: [ master ] + branches: [ master, v2 ] jobs: test: From 4d56898e519105e8cf907ba38724eb6e3b83b158 Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Wed, 5 Apr 2023 22:28:30 +0300 Subject: [PATCH 03/34] chore: linting code (#1533) * chore: make fmt * chore: make lint --- field_parser.go | 1 + field_parserv3.go | 12 ++++----- operation.go | 6 ++--- operationv3.go | 4 +-- parserv3.go | 32 +++++++++++------------ testdata/parseExtension/parseExtension.go | 1 - 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/field_parser.go b/field_parser.go index e317ea9b6..1716e5034 100644 --- a/field_parser.go +++ b/field_parser.go @@ -21,6 +21,7 @@ const ( swaggerTypeTag = "swaggertype" swaggerIgnoreTag = "swaggerignore" ) + var _ FieldParser = &tagBaseFieldParser{} type tagBaseFieldParser struct { diff --git a/field_parserv3.go b/field_parserv3.go index 068067be8..b276a462e 100644 --- a/field_parserv3.go +++ b/field_parserv3.go @@ -457,13 +457,13 @@ func (sf *structFieldV3) parseValidTags(validTag string) { } } -func (field *structFieldV3) parseEnumTags(enumTag string) error { - enumType := field.schemaType - if field.schemaType == ARRAY { - enumType = field.arrayType +func (sf *structFieldV3) parseEnumTags(enumTag string) error { + enumType := sf.schemaType + if sf.schemaType == ARRAY { + enumType = sf.arrayType } - field.enums = nil + sf.enums = nil for _, e := range strings.Split(enumTag, ",") { value, err := defineType(enumType, e) @@ -471,7 +471,7 @@ func (field *structFieldV3) parseEnumTags(enumTag string) error { return err } - field.enums = append(field.enums, value) + sf.enums = append(sf.enums, value) } return nil diff --git a/operation.go b/operation.go index c69b151e3..0b82be77b 100644 --- a/operation.go +++ b/operation.go @@ -1202,9 +1202,9 @@ func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, bool, fileName := entry.Name() - isJson := strings.Contains(fileName, ".json") + isJSON := strings.Contains(fileName, ".json") isYaml := strings.Contains(fileName, ".yaml") - if !isJson && !isYaml { + if !isJSON && !isYaml { continue } @@ -1216,7 +1216,7 @@ func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, bool, return nil, false, fmt.Errorf("Failed to read code example file %s error: %s ", fullPath, err) } - return commentInfo, isJson, nil + return commentInfo, isJSON, nil } } diff --git a/operationv3.go b/operationv3.go index 219171634..e7575fba5 100644 --- a/operationv3.go +++ b/operationv3.go @@ -13,7 +13,7 @@ import ( "gopkg.in/yaml.v2" ) -// Operation describes a single API operation on a path. +// OperationV3 describes a single API operation on a path. // For more information: https://github.com/swaggo/swag#api-operation type OperationV3 struct { parser *Parser @@ -39,7 +39,7 @@ func NewOperationV3(parser *Parser, options ...func(*OperationV3)) *OperationV3 return operation } -// SetCodeExampleFilesDirectory sets the directory to search for codeExamples. +// SetCodeExampleFilesDirectoryV3 sets the directory to search for codeExamples. func SetCodeExampleFilesDirectoryV3(directoryPath string) func(*OperationV3) { return func(o *OperationV3) { o.codeExampleFilesDir = directoryPath diff --git a/parserv3.go b/parserv3.go index b0f49c5df..41a779135 100644 --- a/parserv3.go +++ b/parserv3.go @@ -14,10 +14,10 @@ import ( "github.com/sv-tools/openapi/spec" ) -// FieldParserFactory create FieldParser. +// FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 create FieldParser. type FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 -// FieldParser parse struct field. +// FieldParserV3 parse struct field. type FieldParserV3 interface { ShouldSkip() bool FieldName() (string, error) @@ -28,8 +28,8 @@ type FieldParserV3 interface { } // GetOpenAPI returns *spec.OpenAPI which is the root document object for the API specification. -func (parser *Parser) GetOpenAPI() *spec.OpenAPI { - return parser.openAPI +func (p *Parser) GetOpenAPI() *spec.OpenAPI { + return p.openAPI } func (p *Parser) parseGeneralAPIInfoV3(comments []string) error { @@ -419,8 +419,8 @@ func getSecurityDefinitionKey(lines []string) string { return "" } -// ParseRouterAPIInfo parses router api info for given astFile. -func (parser *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { +// ParseRouterAPIInfoV3 parses router api info for given astFile. +func (p *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { for _, astDescription := range fileInfo.File.Decls { if (fileInfo.ParseFlag & ParseOperations) == ParseNone { continue @@ -431,10 +431,10 @@ func (parser *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { continue } - if parser.matchTags(astDeclaration.Doc.List) && - matchExtension(parser.parseExtension, astDeclaration.Doc.List) { + if p.matchTags(astDeclaration.Doc.List) && + matchExtension(p.parseExtension, astDeclaration.Doc.List) { // for per 'function' comment, create a new 'Operation' object - operation := NewOperationV3(parser, SetCodeExampleFilesDirectoryV3(parser.codeExampleFilesDir)) + operation := NewOperationV3(p, SetCodeExampleFilesDirectoryV3(p.codeExampleFilesDir)) for _, comment := range astDeclaration.Doc.List { err := operation.ParseComment(comment.Text, fileInfo.File) @@ -442,7 +442,7 @@ func (parser *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { return fmt.Errorf("ParseComment error in file %s :%+v", fileInfo.Path, err) } } - err := processRouterOperationV3(parser, operation) + err := processRouterOperationV3(p, operation) if err != nil { return err } @@ -941,8 +941,8 @@ func (p *Parser) getRefTypeSchemaV3(typeSpecDef *TypeSpecDef, schema *SchemaV3) return refSchema } -// GetSchemaTypePath get path of schema type. -func (parser *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], depth int) []string { +// GetSchemaTypePathV3 get path of schema type. +func (p *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], depth int) []string { if schema == nil || depth == 0 { return nil } @@ -955,8 +955,8 @@ func (parser *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], d if name != "" { if pos := strings.LastIndexByte(name, '/'); pos >= 0 { name = name[pos+1:] - if schema, ok := parser.openAPI.Components.Spec.Schemas[name]; ok { - return parser.GetSchemaTypePathV3(schema, depth) + if schema, ok := p.openAPI.Components.Spec.Schemas[name]; ok { + return p.GetSchemaTypePathV3(schema, depth) } } @@ -970,7 +970,7 @@ func (parser *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], d s := []string{schema.Spec.Type[0]} - return append(s, parser.GetSchemaTypePathV3(schema.Spec.Items.Schema, depth)...) + return append(s, p.GetSchemaTypePathV3(schema.Spec.Items.Schema, depth)...) case OBJECT: if schema.Spec.AdditionalProperties != nil && schema.Spec.AdditionalProperties.Schema != nil { // for map @@ -978,7 +978,7 @@ func (parser *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], d s := []string{schema.Spec.Type[0]} - return append(s, parser.GetSchemaTypePathV3(schema.Spec.AdditionalProperties.Schema, depth)...) + return append(s, p.GetSchemaTypePathV3(schema.Spec.AdditionalProperties.Schema, depth)...) } } diff --git a/testdata/parseExtension/parseExtension.go b/testdata/parseExtension/parseExtension.go index c666ebde1..4299895d4 100644 --- a/testdata/parseExtension/parseExtension.go +++ b/testdata/parseExtension/parseExtension.go @@ -12,5 +12,4 @@ func Fun2() {} func Fun3() {} // @Router /with-empty-comment-line [get] -// func FunEmptyCommentLine() {} From 7ab1219df8e1ffc6acb85096befc9206e5a6f9c0 Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Wed, 5 Apr 2023 23:32:18 +0300 Subject: [PATCH 04/34] chore: remove GOPATH support and go1.17 (#1536) --- .github/workflows/ci.yml | 18 ----------- README.md | 2 +- README_zh-CN.md | 2 +- generics.go | 3 -- generics_other.go | 42 ------------------------- generics_other_test.go | 67 ---------------------------------------- generics_test.go | 3 -- 7 files changed, 2 insertions(+), 135 deletions(-) delete mode 100644 generics_other.go delete mode 100644 generics_other_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d16a00809..7dad15e83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,36 +15,18 @@ jobs: runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v3 - with: - path: ./src/github.com/${{ github.repository }} - name: Set up Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: deps - working-directory: ./src/github.com/${{ github.repository }} run: make deps - env: - GOPATH: ${{ runner.workspace }} - name: static program analysis - working-directory: ./src/github.com/${{ github.repository }} run: | - export PATH=$PATH:$(go env GOPATH)/bin # https://github.com/actions/setup-go/issues/14 - mkdir -p $(go env GOPATH)/src/github.com/swaggo - ln -s $(pwd) $(go env GOPATH)/src/github.com/swaggo/swag make fmt-check lint vet - env: - GOPATH: ${{ runner.workspace }} - name: build run: make build - working-directory: ./src/github.com/${{ github.repository }} - env: - GOPATH: ${{ runner.workspace }} - name: test - working-directory: ./src/github.com/${{ github.repository }} run: make test - env: - GOPATH: ${{ runner.workspace }} - name: coverage - working-directory: ./src/github.com/${{ github.repository }} run: bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 4a48fa6ff..36fa48a89 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a varie ```sh go install github.com/swaggo/swag/cmd/swag@latest ``` -To build from source you need [Go](https://golang.org/dl/) (1.17 or newer). +To build from source you need [Go](https://golang.org/dl/) (1.18 or newer). Or download a pre-compiled binary from the [release page](https://github.com/swaggo/swag/releases). diff --git a/README_zh-CN.md b/README_zh-CN.md index 60b6fb559..3ff52d0be 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -50,7 +50,7 @@ Swag将Go的注释转换为Swagger2.0文档。我们为流行的 [Go Web Framewo go install github.com/swaggo/swag/cmd/swag@latest ``` -从源码开始构建的话,需要有Go环境(1.17及以上版本)。 +从源码开始构建的话,需要有Go环境(1.18及以上版本)。 或者从github的release页面下载预编译好的二进制文件。 diff --git a/generics.go b/generics.go index c14b3ab01..c22dbaa8b 100644 --- a/generics.go +++ b/generics.go @@ -1,6 +1,3 @@ -//go:build go1.18 -// +build go1.18 - package swag import ( diff --git a/generics_other.go b/generics_other.go deleted file mode 100644 index 5fd9e8231..000000000 --- a/generics_other.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build !go1.18 -// +build !go1.18 - -package swag - -import ( - "fmt" - "github.com/go-openapi/spec" - "go/ast" -) - -type genericTypeSpec struct { - ArrayDepth int - TypeSpec *TypeSpecDef - Name string -} - -func (pkgDefs *PackagesDefinitions) parametrizeGenericType(file *ast.File, original *TypeSpecDef, fullGenericForm string) *TypeSpecDef { - return original -} - -func getGenericFieldType(file *ast.File, field ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) (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) { - // 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: - 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_test.go b/generics_other_test.go deleted file mode 100644 index 1a396a04b..000000000 --- a/generics_other_test.go +++ /dev/null @@ -1,67 +0,0 @@ -//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.parametrizeGenericType(&ast.File{}, tSpec, "") - assert.Equal(t, tr, tSpec) - - tr = pd.parametrizeGenericType(&ast.File{}, tSpec, "") - 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 8fa7bdfc3..bd79f93de 100644 --- a/generics_test.go +++ b/generics_test.go @@ -1,6 +1,3 @@ -//go:build go1.18 -// +build go1.18 - package swag import ( From b980cd969545cc58f7056dd18aadf02eeca4cc20 Mon Sep 17 00:00:00 2001 From: Nerzal Date: Wed, 5 Apr 2023 18:19:12 +0200 Subject: [PATCH 05/34] Implementing OpenAPI 3.1.0 spec (#1513) * fix x-tagGroups * fix module name * change paths * refactoring * update dependencies * stuff * add log * fix finding of main file * fix broken type resolution * fix bug * clean deps * fix tool after merging upstream * use json-iterator to marshal json * fix generating of json examples * update config used by jsoniter * bump version * update dependencies * resolve merge conflicts * use newest go in docker * yep * fix gen * fix gen * update swag version * yep * fix parser * fix some tests * fix all tests * parse most of general api description * implement security scheme parsing * parse oauth2 specs * parse scopes and extensions in security schemes * extend parsing security stuff * process v3 routes * meh * find unimported types * parse basic operation info * parse primitive and object parameters * generate openapi spec * fix module name * cleanup * update version to 2.0 * fix issues that appread after merging * cleanup after merge conflicts * fix all tests * add go 1.19 to workflows * pin dockerfile to 1.19.7 * Set minimum supported Go version to 1.18.x * parse response headers * copy readme * started to implement field parser * Refactor: use RefOrSpec instead of Spec * start to add tests for operationv3 * fix tests * implement allOf with primitive types * Add NestedPrimitiveArrayType test * implement TestParseResponseCommentWithNestedFieldsV3 * add more tests * parse arrays and maps * fix implementation of map types * implement more tests * fix example docs * adjust example * fix example jsons * support array types in Parameters * implement more tests, implement correct collectionFormat handling * finish implementation of operationv3 tests * all tests green * fix parsing of security definitions * add test for generalAPI info * end of day checkin * Update example.json * fix codeSamples from file and fix creation of operations * fix resolving of schema ref errors * fix tests that broke due to fixes on model parsing * Fix creating schemes of array types of custom objects * Fix resolution of refSchemas * cleanup * update dependencies * cleanup * Update README.md reset readme.md * Update README_zh-CN.md reset readme_zh-CN * update dependency * reset test file --------- Co-authored-by: Tobias Theel --- .github/workflows/ci.yml | 2 +- .gitignore | 3 + cmd/swag/main.go | 12 +- enums_test.go | 35 +- example/celler/controller/bottles.go | 1 - example/markdown/go.sum | 19 +- example/markdown/main.go | 29 +- example/object-map-example/go.mod | 8 +- example/object-map-example/go.sum | 20 +- extensions.go | 4 + field_parser.go | 6 + field_parserv3.go | 572 ++++++++ gen/gen.go | 64 +- gen/gen_test.go | 1 - gen/genv3.go | 202 +++ genericsv3.go | 34 + go.mod | 32 +- go.sum | 90 +- operation.go | 37 +- operation_test.go | 13 +- operationv3.go | 993 +++++++++++++ operationv3_test.go | 1916 +++++++++++++++++++++++++ packages.go | 42 +- packages_test.go | 2 +- parser.go | 318 ++-- parser_test.go | 2 +- parserv3.go | 995 +++++++++++++ parserv3_test.go | 369 +++++ schemav3.go | 141 ++ testdata/code_examples/example.json | 10 +- testdata/generics_property/api/api.go | 3 +- testdata/v3/extensionsFail1.go | 9 + testdata/v3/extensionsFail2.go | 9 + testdata/v3/main.go | 64 + testdata/v3/pet/main.go | 20 + testdata/v3/pet/web/handler.go | 38 + testdata/v3/simple/api/api.go | 140 ++ testdata/v3/simple/cross/test.go | 6 + testdata/v3/simple/main.go | 28 + testdata/v3/simple/web/handler.go | 101 ++ typesv3.go | 10 + version.go | 2 +- 42 files changed, 6134 insertions(+), 268 deletions(-) create mode 100644 extensions.go create mode 100644 field_parserv3.go create mode 100644 gen/genv3.go create mode 100644 genericsv3.go create mode 100644 operationv3.go create mode 100644 operationv3_test.go create mode 100644 parserv3.go create mode 100644 parserv3_test.go create mode 100644 schemav3.go create mode 100644 testdata/v3/extensionsFail1.go create mode 100644 testdata/v3/extensionsFail2.go create mode 100644 testdata/v3/main.go create mode 100644 testdata/v3/pet/main.go create mode 100644 testdata/v3/pet/web/handler.go create mode 100644 testdata/v3/simple/api/api.go create mode 100644 testdata/v3/simple/cross/test.go create mode 100644 testdata/v3/simple/main.go create mode 100644 testdata/v3/simple/web/handler.go create mode 100644 typesv3.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2ea781d7..9e129e778 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: test: strategy: matrix: - go: [ '1.17.x', '1.18.x', '1.19.x', '1.20.x' ] + go: [ '1.18.x', '1.19.x', '1.20.x' ] platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/.gitignore b/.gitignore index 6ba1ec223..2438076f8 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ cover.out /swag /swag.exe +cmd/swag/docs/* + +.vscode/launch.json \ No newline at end of file diff --git a/cmd/swag/main.go b/cmd/swag/main.go index 1b325865e..f2164e10d 100644 --- a/cmd/swag/main.go +++ b/cmd/swag/main.go @@ -7,11 +7,11 @@ import ( "os" "strings" - "github.com/urfave/cli/v2" - "github.com/swaggo/swag" "github.com/swaggo/swag/format" "github.com/swaggo/swag/gen" + + "github.com/urfave/cli/v2" ) const ( @@ -35,6 +35,7 @@ const ( quietFlag = "quiet" tagsFlag = "tags" parseExtensionFlag = "parseExtension" + openAPIVersionFlag = "v3.1" packageName = "packageName" collectionFormatFlag = "collectionFormat" ) @@ -143,6 +144,11 @@ var initFlags = []cli.Flag{ Value: "", Usage: "A comma-separated list of tags to filter the APIs for which the documentation is generated.Special case if the tag is prefixed with the '!' character then the APIs with that tag will be excluded", }, + &cli.BoolFlag{ + Name: openAPIVersionFlag, + Value: false, + Usage: "Generate OpenAPI V3.1 spec", + }, &cli.StringFlag{ Name: packageName, Value: "", @@ -201,11 +207,13 @@ func initAction(ctx *cli.Context) error { Tags: ctx.String(tagsFlag), PackageName: ctx.String(packageName), Debugger: logger, + OpenAPIVersion: ctx.Bool(openAPIVersionFlag), CollectionFormat: collectionFormat, }) } func main() { + fmt.Println("Swag version: ", swag.Version) app := cli.NewApp() app.Version = swag.Version app.Usage = "Automatically generate RESTful API documentation with Swagger 2.0 for Go." diff --git a/enums_test.go b/enums_test.go index dbae10d83..1d8e1930f 100644 --- a/enums_test.go +++ b/enums_test.go @@ -1,32 +1,29 @@ package swag import ( - "encoding/json" - "os" - "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseGlobalEnums(t *testing.T) { searchDir := "testdata/enums" - expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) - assert.NoError(t, err) p := New() - 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)) - constsPath := "github.com/swaggo/swag/testdata/enums/consts" - assert.Equal(t, 64, p.packages.packages[constsPath].ConstTable["uintSize"].Value) - assert.Equal(t, int32(62), p.packages.packages[constsPath].ConstTable["maxBase"].Value) - assert.Equal(t, 8, p.packages.packages[constsPath].ConstTable["shlByLen"].Value) - assert.Equal(t, 255, p.packages.packages[constsPath].ConstTable["hexnum"].Value) - assert.Equal(t, 15, p.packages.packages[constsPath].ConstTable["octnum"].Value) - assert.Equal(t, `aa\nbb\u8888cc`, p.packages.packages[constsPath].ConstTable["nonescapestr"].Value) - assert.Equal(t, "aa\nbb\u8888cc", p.packages.packages[constsPath].ConstTable["escapestr"].Value) - assert.Equal(t, '\u8888', p.packages.packages[constsPath].ConstTable["escapechar"].Value) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + require.NoError(t, err) + + const constsPath = "github.com/swaggo/swag/testdata/enums/consts" + table := p.packages.packages[constsPath].ConstTable + require.NotNil(t, table, "const table must not be nil") + + assert.Equal(t, 64, table["uintSize"].Value) + assert.Equal(t, int32(62), table["maxBase"].Value) + assert.Equal(t, 8, table["shlByLen"].Value) + assert.Equal(t, 255, table["hexnum"].Value) + assert.Equal(t, 15, table["octnum"].Value) + assert.Equal(t, `aa\nbb\u8888cc`, table["nonescapestr"].Value) + assert.Equal(t, "aa\nbb\u8888cc", table["escapestr"].Value) + assert.Equal(t, '\u8888', table["escapechar"].Value) } diff --git a/example/celler/controller/bottles.go b/example/celler/controller/bottles.go index 925414d32..1a7907d5c 100644 --- a/example/celler/controller/bottles.go +++ b/example/celler/controller/bottles.go @@ -5,7 +5,6 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/swaggo/swag/example/celler/httputil" "github.com/swaggo/swag/example/celler/model" ) diff --git a/example/markdown/go.sum b/example/markdown/go.sum index b08a39e07..cb3034a03 100644 --- a/example/markdown/go.sum +++ b/example/markdown/go.sum @@ -67,15 +67,12 @@ github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pA github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -83,12 +80,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -97,26 +92,20 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= diff --git a/example/markdown/main.go b/example/markdown/main.go index a13720c27..674fbdf8e 100644 --- a/example/markdown/main.go +++ b/example/markdown/main.go @@ -1,31 +1,30 @@ package main import ( - "net/http" - "github.com/gorilla/mux" httpSwagger "github.com/swaggo/http-swagger" "github.com/swaggo/swag/example/markdown/api" _ "github.com/swaggo/swag/example/markdown/docs" + "net/http" ) -// @title Swagger Example API -// @version 1.0 -// @description This is a sample server Petstore server. -// @description.markdown -// @termsOfService http://swagger.io/terms/ +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @description.markdown +// @termsOfService http://swagger.io/terms/ -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @tag.name admin -// @tag.description.markdown +// @tag.name admin +// @tag.description.markdown -// @BasePath /v2 +// @BasePath /v2 func main() { router := mux.NewRouter() diff --git a/example/object-map-example/go.mod b/example/object-map-example/go.mod index 1c8b2e5ee..57995e45f 100644 --- a/example/object-map-example/go.mod +++ b/example/object-map-example/go.mod @@ -25,12 +25,12 @@ require ( github.com/leodido/go-urn v1.2.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.12 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect - github.com/ugorji/go/codec v1.1.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/ugorji/go/codec v1.1.13 // indirect golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.5.0 // indirect - golang.org/x/tools v0.1.10 // indirect + golang.org/x/tools v0.1.12 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/example/object-map-example/go.sum b/example/object-map-example/go.sum index ff15136db..bac00b11c 100644 --- a/example/object-map-example/go.sum +++ b/example/object-map-example/go.sum @@ -61,10 +61,12 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= @@ -92,13 +94,14 @@ github.com/swaggo/gin-swagger v1.4.2/go.mod h1:hmJ1vPn+XjUvnbzjCdUAxVqgraxELxk8x github.com/swaggo/swag v1.7.9/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU= github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go v1.1.13 h1:nB3O5kBSQGjEQAcfe1aLUYuxmXdFKmYgBZhY32rQb6Q= +github.com/ugorji/go v1.1.13/go.mod h1:jxau1n+/wyTGLQoCkjok9r5zFa/FxT6eI5HiHKQszjc= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.1.13 h1:013LbFhocBoIqgHeIHKlV4JWYhqogATYWZhIcH0WHn4= +github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -107,8 +110,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -117,7 +118,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -134,7 +134,6 @@ golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -153,13 +152,10 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/extensions.go b/extensions.go new file mode 100644 index 000000000..358104cac --- /dev/null +++ b/extensions.go @@ -0,0 +1,4 @@ +package swag + +// CodeSamples is used to parse code samples. +type CodeSamples []map[string]string diff --git a/field_parser.go b/field_parser.go index 98ad0ddc7..e317ea9b6 100644 --- a/field_parser.go +++ b/field_parser.go @@ -21,6 +21,7 @@ const ( swaggerTypeTag = "swaggertype" swaggerIgnoreTag = "swaggerignore" ) +var _ FieldParser = &tagBaseFieldParser{} type tagBaseFieldParser struct { p *Parser @@ -519,6 +520,7 @@ func (ps *tagBaseFieldParser) IsRequired() (bool, error) { } func parseValidTags(validTag string, sf *structField) { + // `validate:"required,max=10,min=1"` // ps. required checked by IsRequired(). for _, val := range strings.Split(validTag, ",") { @@ -541,6 +543,10 @@ func parseValidTags(validTag string, sf *structField) { case "min", "gte": sf.setMin(valValue) case "oneof": + if strings.Contains(validTag, "swaggerIgnore") { + continue + } + sf.setOneOf(valValue) case "unique": if sf.schemaType == ARRAY { diff --git a/field_parserv3.go b/field_parserv3.go new file mode 100644 index 000000000..068067be8 --- /dev/null +++ b/field_parserv3.go @@ -0,0 +1,572 @@ +package swag + +import ( + "fmt" + "go/ast" + "reflect" + "strconv" + "strings" + + "github.com/sv-tools/openapi/spec" +) + +type structFieldV3 struct { + schemaType string + arrayType string + formatType string + maximum *int + minimum *int + multipleOf *int + maxLength *int + minLength *int + maxItems *int + minItems *int + exampleValue interface{} + enums []interface{} + enumVarNames []interface{} + unique bool +} + +func (sf *structFieldV3) setOneOf(valValue string) { + if len(sf.enums) != 0 { + return + } + + enumType := sf.schemaType + if sf.schemaType == ARRAY { + enumType = sf.arrayType + } + + valValues := parseOneOfParam2(valValue) + for i := range valValues { + value, err := defineType(enumType, valValues[i]) + if err != nil { + continue + } + + sf.enums = append(sf.enums, value) + } +} + +func (sf *structFieldV3) setMin(valValue string) { + value, err := strconv.Atoi(valValue) + if err != nil { + return + } + + switch sf.schemaType { + case INTEGER, NUMBER: + sf.minimum = &value + case STRING: + sf.minLength = &value + case ARRAY: + sf.minItems = &value + } +} + +func (sf *structFieldV3) setMax(valValue string) { + value, err := strconv.Atoi(valValue) + if err != nil { + return + } + + switch sf.schemaType { + case INTEGER, NUMBER: + sf.maximum = &value + case STRING: + sf.maxLength = &value + case ARRAY: + sf.maxItems = &value + } +} + +type tagBaseFieldParserV3 struct { + p *Parser + field *ast.Field + tag reflect.StructTag +} + +func newTagBaseFieldParserV3(p *Parser, field *ast.Field) FieldParserV3 { + fieldParser := tagBaseFieldParserV3{ + p: p, + field: field, + tag: "", + } + if fieldParser.field.Tag != nil { + fieldParser.tag = reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", "")) + } + + return &fieldParser +} + +func (ps *tagBaseFieldParserV3) CustomSchema() (*spec.RefOrSpec[spec.Schema], error) { + if ps.field.Tag == nil { + return nil, nil + } + + typeTag := ps.tag.Get(swaggerTypeTag) + if typeTag != "" { + return BuildCustomSchemaV3(strings.Split(typeTag, ",")) + } + + return nil, nil +} + +// ComplementSchema complement schema with field properties +func (ps *tagBaseFieldParserV3) ComplementSchema(schema *spec.RefOrSpec[spec.Schema]) error { + if schema.Spec == nil { + schema = ps.p.openAPI.Components.Spec.Schemas[strings.ReplaceAll(schema.Ref.Ref, "#/components/schemas/", "")] + if schema == nil { + return fmt.Errorf("could not resolve schema for ref %s", schema.Ref.Ref) + } + } + + types := ps.p.GetSchemaTypePathV3(schema, 2) + if len(types) == 0 { + return fmt.Errorf("invalid type for field: %s", ps.field.Names[0]) + } + + if schema.Ref != nil { //IsRefSchema(schema) + // TODO fetch existing schema from components + var newSchema = spec.Schema{} + err := ps.complementSchema(&newSchema, types) + if err != nil { + return err + } + // if !reflect.ValueOf(newSchema).IsZero() { + // *schema = *(newSchema.WithAllOf(*schema.Spec)) + // } + return nil + } + + return ps.complementSchema(schema.Spec, types) +} + +// complementSchema complement schema with field properties +func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []string) error { + if ps.field.Tag == nil { + if ps.field.Doc != nil { + schema.Description = strings.TrimSpace(ps.field.Doc.Text()) + } + + if schema.Description == "" && ps.field.Comment != nil { + schema.Description = strings.TrimSpace(ps.field.Comment.Text()) + } + + return nil + } + + field := &structFieldV3{ + schemaType: types[0], + formatType: ps.tag.Get(formatTag), + } + + if len(types) > 1 && (types[0] == ARRAY || types[0] == OBJECT) { + field.arrayType = types[1] + } + + jsonTagValue := ps.tag.Get(jsonTag) + + bindingTagValue := ps.tag.Get(bindingTag) + if bindingTagValue != "" { + field.parseValidTags(bindingTagValue) + } + + validateTagValue := ps.tag.Get(validateTag) + if validateTagValue != "" { + field.parseValidTags(validateTagValue) + } + + enumsTagValue := ps.tag.Get(enumsTag) + if enumsTagValue != "" { + err := field.parseEnumTags(enumsTagValue) + if err != nil { + return err + } + } + + if IsNumericType(field.schemaType) || IsNumericType(field.arrayType) { + maximum, err := getIntTagV3(ps.tag, maximumTag) + if err != nil { + return err + } + + if maximum != nil { + field.maximum = maximum + } + + minimum, err := getIntTagV3(ps.tag, minimumTag) + if err != nil { + return err + } + + if minimum != nil { + field.minimum = minimum + } + + multipleOf, err := getIntTagV3(ps.tag, multipleOfTag) + if err != nil { + return err + } + + if multipleOf != nil { + field.multipleOf = multipleOf + } + } + + if field.schemaType == STRING || field.arrayType == STRING { + maxLength, err := getIntTagV3(ps.tag, maxLengthTag) + if err != nil { + return err + } + + if maxLength != nil { + field.maxLength = maxLength + } + + minLength, err := getIntTagV3(ps.tag, minLengthTag) + if err != nil { + return err + } + + if minLength != nil { + field.minLength = minLength + } + } + + // json:"name,string" or json:",string" + exampleTagValue, ok := ps.tag.Lookup(exampleTag) + if ok { + field.exampleValue = exampleTagValue + + if !strings.Contains(jsonTagValue, ",string") { + example, err := defineTypeOfExample(field.schemaType, field.arrayType, exampleTagValue) + if err != nil { + return err + } + + field.exampleValue = example + } + } + + // perform this after setting everything else (min, max, etc...) + if strings.Contains(jsonTagValue, ",string") { + // @encoding/json: "It applies only to fields of string, floating point, integer, or boolean types." + defaultValues := map[string]string{ + // Zero Values as string + STRING: "", + INTEGER: "0", + BOOLEAN: "false", + NUMBER: "0", + } + + defaultValue, ok := defaultValues[field.schemaType] + if ok { + field.schemaType = STRING + *schema = *PrimitiveSchemaV3(field.schemaType).Spec + + if field.exampleValue == nil { + // if exampleValue is not defined by the user, + // we will force an example with a correct value + // (eg: int->"0", bool:"false") + field.exampleValue = defaultValue + } + } + } + + if ps.field.Doc != nil { + schema.Description = strings.TrimSpace(ps.field.Doc.Text()) + } + + if schema.Description == "" && ps.field.Comment != nil { + schema.Description = strings.TrimSpace(ps.field.Comment.Text()) + } + + schema.ReadOnly = ps.tag.Get(readOnlyTag) == "true" + + defaultTagValue := ps.tag.Get(defaultTag) + if defaultTagValue != "" { + value, err := defineType(field.schemaType, defaultTagValue) + if err != nil { + return err + } + + schema.Default = value + } + + schema.Example = field.exampleValue + + if field.schemaType != ARRAY { + schema.Format = field.formatType + } + + extensionsTagValue := ps.tag.Get(extensionsTag) + if extensionsTagValue != "" { + schema.Extensions = setExtensionParam(extensionsTagValue) + } + + varNamesTag := ps.tag.Get("x-enum-varnames") + if varNamesTag != "" { + varNames := strings.Split(varNamesTag, ",") + if len(varNames) != len(field.enums) { + return fmt.Errorf("invalid count of x-enum-varnames. expected %d, got %d", len(field.enums), len(varNames)) + } + + field.enumVarNames = nil + + for _, v := range varNames { + field.enumVarNames = append(field.enumVarNames, v) + } + + if field.schemaType == ARRAY { + // Add the var names in the items schema + if schema.Items.Schema.Spec.Extensions == nil { + schema.Items.Schema.Spec.Extensions = map[string]interface{}{} + } + schema.Items.Schema.Spec.Extensions[enumVarNamesExtension] = field.enumVarNames + } else { + // Add to top level schema + if schema.Extensions == nil { + schema.Extensions = map[string]interface{}{} + } + schema.Extensions[enumVarNamesExtension] = field.enumVarNames + } + } + + elemSchema := schema + + if field.schemaType == ARRAY { + // For Array only + schema.MaxItems = field.maxItems + schema.MinItems = field.minItems + schema.UniqueItems = &field.unique + + elemSchema = schema.Items.Schema.Spec + if elemSchema == nil { + elemSchema = ps.p.getSchemaByRef(schema.Items.Schema.Ref) + } + + elemSchema.Format = field.formatType + } + + elemSchema.Maximum = field.maximum + elemSchema.Minimum = field.minimum + elemSchema.MultipleOf = field.multipleOf + elemSchema.MaxLength = field.maxLength + elemSchema.MinLength = field.minLength + elemSchema.Enum = field.enums + + return nil +} + +func getIntTagV3(structTag reflect.StructTag, tagName string) (*int, error) { + strValue := structTag.Get(tagName) + if strValue == "" { + return nil, nil + } + + value, err := strconv.Atoi(strValue) + if err != nil { + return nil, fmt.Errorf("can't parse numeric value of %q tag: %v", tagName, err) + } + + return &value, nil +} + +func parseValidTagsV3(validTag string, sf *structFieldV3) { + + // `validate:"required,max=10,min=1"` + // ps. required checked by IsRequired(). + for _, val := range strings.Split(validTag, ",") { + var ( + valValue string + keyVal = strings.Split(val, "=") + ) + + switch len(keyVal) { + case 1: + case 2: + valValue = strings.ReplaceAll(strings.ReplaceAll(keyVal[1], utf8HexComma, ","), utf8Pipe, "|") + default: + continue + } + + switch keyVal[0] { + case "max", "lte": + sf.setMax(valValue) + case "min", "gte": + sf.setMin(valValue) + case "oneof": + if strings.Contains(validTag, "swaggerIgnore") { + continue + } + + sf.setOneOf(valValue) + case "unique": + if sf.schemaType == ARRAY { + sf.unique = true + } + case "dive": + // ignore dive + return + default: + continue + } + } +} + +func (sf *structFieldV3) parseValidTags(validTag string) { + + // `validate:"required,max=10,min=1"` + // ps. required checked by IsRequired(). + for _, val := range strings.Split(validTag, ",") { + var ( + valValue string + keyVal = strings.Split(val, "=") + ) + + switch len(keyVal) { + case 1: + case 2: + valValue = strings.ReplaceAll(strings.ReplaceAll(keyVal[1], utf8HexComma, ","), utf8Pipe, "|") + default: + continue + } + + switch keyVal[0] { + case "max", "lte": + sf.setMax(valValue) + case "min", "gte": + sf.setMin(valValue) + case "oneof": + if strings.Contains(validTag, "swaggerIgnore") { + continue + } + + sf.setOneOf(valValue) + case "unique": + if sf.schemaType == ARRAY { + sf.unique = true + } + case "dive": + // ignore dive + return + default: + continue + } + } +} + +func (field *structFieldV3) parseEnumTags(enumTag string) error { + enumType := field.schemaType + if field.schemaType == ARRAY { + enumType = field.arrayType + } + + field.enums = nil + + for _, e := range strings.Split(enumTag, ",") { + value, err := defineType(enumType, e) + if err != nil { + return err + } + + field.enums = append(field.enums, value) + } + + return nil +} + +func (ps *tagBaseFieldParserV3) ShouldSkip() bool { + // Skip non-exported fields. + if ps.field.Names != nil && !ast.IsExported(ps.field.Names[0].Name) { + return true + } + + if ps.field.Tag == nil { + return false + } + + ignoreTag := ps.tag.Get(swaggerIgnoreTag) + if strings.EqualFold(ignoreTag, "true") { + return true + } + + // json:"tag,hoge" + name := strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0]) + if name == "-" { + return true + } + + return false +} + +func (ps *tagBaseFieldParserV3) FieldName() (string, error) { + var name string + + if ps.field.Tag != nil { + // json:"tag,hoge" + name = strings.TrimSpace(strings.Split(ps.tag.Get(jsonTag), ",")[0]) + if name != "" { + return name, nil + } + + // use "form" tag over json tag + name = ps.FormName() + if name != "" { + return name, nil + } + } + + if ps.field.Names == nil { + return "", nil + } + + switch ps.p.PropNamingStrategy { + case SnakeCase: + return toSnakeCase(ps.field.Names[0].Name), nil + case PascalCase: + return ps.field.Names[0].Name, nil + default: + return toLowerCamelCase(ps.field.Names[0].Name), nil + } +} + +func (ps *tagBaseFieldParserV3) FormName() string { + if ps.field.Tag != nil { + return strings.TrimSpace(strings.Split(ps.tag.Get(formTag), ",")[0]) + } + return "" +} + +func (ps *tagBaseFieldParserV3) IsRequired() (bool, error) { + if ps.field.Tag == nil { + return false, nil + } + + bindingTag := ps.tag.Get(bindingTag) + if bindingTag != "" { + for _, val := range strings.Split(bindingTag, ",") { + switch val { + case requiredLabel: + return true, nil + case optionalLabel: + return false, nil + } + } + } + + validateTag := ps.tag.Get(validateTag) + if validateTag != "" { + for _, val := range strings.Split(validateTag, ",") { + switch val { + case requiredLabel: + return true, nil + case optionalLabel: + return false, nil + } + } + } + + return ps.p.RequiredByDefault, nil +} diff --git a/gen/gen.go b/gen/gen.go index 21111bbe9..ccbd97b99 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -15,7 +15,10 @@ import ( "text/template" "time" + jsoniter "github.com/json-iterator/go" + "github.com/go-openapi/spec" + openapi "github.com/sv-tools/openapi/spec" "github.com/swaggo/swag" "sigs.k8s.io/yaml" ) @@ -29,11 +32,12 @@ type genTypeWriter func(*Config, *spec.Swagger) error // Gen presents a generate tool for swag. type Gen struct { - json func(data interface{}) ([]byte, error) - jsonIndent func(data interface{}) ([]byte, error) - jsonToYAML func(data []byte) ([]byte, error) - outputTypeMap map[string]genTypeWriter - debug Debugger + json func(data interface{}) ([]byte, error) + jsonIndent func(data interface{}) ([]byte, error) + jsonToYAML func(data []byte) ([]byte, error) + outputTypeMap map[string]genTypeWriter + outputTypeMapV3 map[string]openAPITypeWriter + debug Debugger } // Debugger is the interface that wraps the basic Printf method. @@ -46,7 +50,8 @@ func New() *Gen { gen := Gen{ json: json.Marshal, jsonIndent: func(data interface{}) ([]byte, error) { - return json.MarshalIndent(data, "", " ") + var json = jsoniter.ConfigCompatibleWithStandardLibrary + return json.MarshalIndent(&data, "", " ") }, jsonToYAML: yaml.JSONToYAML, debug: log.New(os.Stdout, "", log.LstdFlags), @@ -59,6 +64,13 @@ func New() *Gen { "yml": gen.writeYAMLSwagger, } + gen.outputTypeMapV3 = map[string]openAPITypeWriter{ + "go": gen.writeDocOpenAPI, + "json": gen.writeJSONOpenAPI, + "yaml": gen.writeYAMLOpenAPI, + "yml": gen.writeYAMLOpenAPI, + } + return &gen } @@ -127,6 +139,8 @@ type Config struct { // include only tags mentioned when searching, comma separated Tags string + // if true, OpenAPI V3.1 spec will be generated + OpenAPIVersion bool // PackageName defines package name of generated `docs.go` PackageName string @@ -182,6 +196,7 @@ func (g *Gen) Build(config *Config) error { swag.SetOverrides(overrides), swag.ParseUsingGoList(config.ParseGoList), swag.SetTags(config.Tags), + swag.SetOpenAPIVersion(config.OpenAPIVersion), swag.SetCollectionFormat(config.CollectionFormat), ) @@ -194,12 +209,45 @@ func (g *Gen) Build(config *Config) error { return err } - swagger := p.GetSwagger() - if err := os.MkdirAll(config.OutputDir, os.ModePerm); err != nil { return err } + if config.OpenAPIVersion { + openAPI := p.GetOpenAPI() + err := g.writeOpenAPI(config, openAPI) + if err != nil { + return err + } + + return nil + } + + swagger := p.GetSwagger() + err := g.writeSwagger(config, swagger) + if err != nil { + return err + } + + return nil +} + +func (g *Gen) writeOpenAPI(config *Config, o *openapi.OpenAPI) error { + for _, outputType := range config.OutputTypes { + outputType = strings.ToLower(strings.TrimSpace(outputType)) + if typeWriter, ok := g.outputTypeMapV3[outputType]; ok { + if err := typeWriter(config, o); err != nil { + return err + } + } else { + log.Printf("output type '%s' not supported", outputType) + } + } + + return nil +} + +func (g *Gen) writeSwagger(config *Config, swagger *spec.Swagger) error { for _, outputType := range config.OutputTypes { outputType = strings.ToLower(strings.TrimSpace(outputType)) if typeWriter, ok := g.outputTypeMap[outputType]; ok { diff --git a/gen/gen_test.go b/gen/gen_test.go index 35acd6bf3..62a313e6a 100644 --- a/gen/gen_test.go +++ b/gen/gen_test.go @@ -224,7 +224,6 @@ func TestGen_BuildDescriptionWithQuotes(t *testing.T) { require.NoError(t, err) } } - cmd := exec.Command("go", "build", "-buildmode=plugin", "github.com/swaggo/swag/testdata/quotes") cmd.Dir = config.SearchDir diff --git a/gen/genv3.go b/gen/genv3.go new file mode 100644 index 000000000..8010660f2 --- /dev/null +++ b/gen/genv3.go @@ -0,0 +1,202 @@ +package gen + +import ( + "bytes" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + "text/template" + "time" + + "github.com/sv-tools/openapi/spec" + "github.com/swaggo/swag" +) + +type openAPITypeWriter func(*Config, *spec.OpenAPI) error + +func (g *Gen) writeDocOpenAPI(config *Config, openAPI *spec.OpenAPI) error { + var filename = "docs.go" + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + docFileName := path.Join(config.OutputDir, filename) + + absOutputDir, err := filepath.Abs(config.OutputDir) + if err != nil { + return err + } + + packageName := filepath.Base(absOutputDir) + + docs, err := os.Create(docFileName) + if err != nil { + return err + } + defer docs.Close() + + // Write doc + err = g.writeGoDocV3(packageName, docs, openAPI, config) + if err != nil { + return err + } + + g.debug.Printf("create docs.go at %+v", docFileName) + + return nil +} + +func (g *Gen) writeJSONOpenAPI(config *Config, swagger *spec.OpenAPI) error { + var filename = "swagger.json" + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + jsonFileName := path.Join(config.OutputDir, filename) + + b, err := g.jsonIndent(swagger) + if err != nil { + return err + } + + err = g.writeFile(b, jsonFileName) + if err != nil { + return err + } + + g.debug.Printf("create swagger.json at %+v", jsonFileName) + + return nil +} + +func (g *Gen) writeYAMLOpenAPI(config *Config, swagger *spec.OpenAPI) error { + var filename = "swagger.yaml" + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + yamlFileName := path.Join(config.OutputDir, filename) + + b, err := g.json(swagger) + if err != nil { + return err + } + + y, err := g.jsonToYAML(b) + if err != nil { + return fmt.Errorf("cannot covert json to yaml error: %s", err) + } + + err = g.writeFile(y, yamlFileName) + if err != nil { + return err + } + + g.debug.Printf("create swagger.yaml at %+v", yamlFileName) + + return nil +} + +func (g *Gen) writeGoDocV3(packageName string, output io.Writer, openAPI *spec.OpenAPI, config *Config) error { + generator, err := template.New("swagger_info").Funcs(template.FuncMap{ + "printDoc": func(v string) string { + // Add schemes + v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:] + // Sanitize backticks + return strings.Replace(v, "`", "`+\"`\"+`", -1) + }, + }).Parse(packageTemplateV3) + if err != nil { + return err + } + + openAPISpec := spec.OpenAPI{ + Components: openAPI.Components, + OpenAPI: openAPI.OpenAPI, + Info: &spec.Extendable[spec.Info]{ + Spec: &spec.Info{ + Description: "{{escape .Description}}", + Title: "{{.Title}}", + Version: "{{.Version}}", + TermsOfService: openAPI.Info.Spec.TermsOfService, + Contact: openAPI.Info.Spec.Contact, + License: openAPI.Info.Spec.License, + Summary: openAPI.Info.Spec.Summary, + }, + Extensions: openAPI.Info.Extensions, + }, + ExternalDocs: openAPI.ExternalDocs, + Paths: openAPI.Paths, + WebHooks: openAPI.WebHooks, + JsonSchemaDialect: openAPI.JsonSchemaDialect, + Security: openAPI.Security, + Tags: openAPI.Tags, + Servers: openAPI.Servers, + } + + // crafted docs.json + buf, err := g.jsonIndent(openAPISpec) + if err != nil { + return err + } + + buffer := &bytes.Buffer{} + + err = generator.Execute(buffer, struct { + Timestamp time.Time + Doc string + PackageName string + Title string + Description string + Version string + InstanceName string + GeneratedTime bool + }{ + Timestamp: time.Now(), + GeneratedTime: config.GeneratedTime, + Doc: string(buf), + PackageName: packageName, + Title: openAPI.Info.Spec.Title, + Description: openAPI.Info.Spec.Description, + Version: openAPI.Info.Spec.Version, + InstanceName: config.InstanceName, + }) + if err != nil { + return err + } + + code := g.formatSource(buffer.Bytes()) + + // write + _, err = output.Write(code) + + return err +} + +var packageTemplateV3 = `// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT + +package docs + +import "github.com/swaggo/swag" + +const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = ` + "`{{ printDoc .Doc}}`" + ` + +// SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} holds exported Swagger Info so clients can modify it +var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = &swag.Spec{ + Version: {{ printf "%q" .Version}}, + Title: {{ printf "%q" .Title}}, + Description: {{ printf "%q" .Description}}, + InfoInstanceName: {{ printf "%q" .InstanceName }}, + SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, +} + +func init() { + swag.Register(SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}) +} +` diff --git a/genericsv3.go b/genericsv3.go new file mode 100644 index 000000000..1306a3622 --- /dev/null +++ b/genericsv3.go @@ -0,0 +1,34 @@ +package swag + +import ( + "go/ast" + + "github.com/sv-tools/openapi/spec" +) + +func (p *Parser) parseGenericTypeExprV3(file *ast.File, typeExpr ast.Expr) (*spec.RefOrSpec[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, *ast.IndexListExpr: + name, err := getExtendedGenericFieldType(file, expr, nil) + if err == nil { + if schema, err := p.getTypeSchemaV3(name, file, false); err == nil { + return schema, nil + } + } + + p.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead. (%s)\n", typeExpr, err) + default: + p.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) + } + + return PrimitiveSchemaV3(OBJECT), nil +} diff --git a/go.mod b/go.mod index 52fa6b894..67772b27f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,12 @@ go 1.18 require ( github.com/KyleBanks/depth v1.2.1 + github.com/go-openapi/spec v0.20.8 + github.com/json-iterator/go v1.1.12 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.8.2 + github.com/sv-tools/openapi v0.2.1 + golang.org/x/tools v0.7.0 github.com/go-openapi/spec v0.20.4 github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 @@ -12,21 +18,27 @@ require ( ) require ( - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect +) + +require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.6 // indirect - github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/mailru/easyjson v0.7.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/urfave/cli/v2 v2.25.1 + golang.org/x/sys v0.7.0 // indirect + gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - golang.org/x/net v0.8.0 // indirect golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect ) diff --git a/go.sum b/go.sum index 8ef14012f..690928581 100644 --- a/go.sum +++ b/go.sum @@ -1,78 +1,80 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU= +github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA= +github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg= +github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= +github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/operation.go b/operation.go index 3485f51f4..97523714f 100644 --- a/operation.go +++ b/operation.go @@ -6,6 +6,7 @@ import ( "go/ast" goparser "go/parser" "go/token" + "log" "net/http" "os" "path/filepath" @@ -15,6 +16,7 @@ import ( "github.com/go-openapi/spec" "golang.org/x/tools/go/loader" + "gopkg.in/yaml.v2" ) // RouteProperties describes HTTP properties of a single router comment. @@ -160,17 +162,28 @@ func (operation *Operation) ParseComment(comment string, astFile *ast.File) erro // ParseCodeSample godoc. func (operation *Operation) ParseCodeSample(attribute, _, lineRemainder string) error { + log.Println("line remainder:", lineRemainder) + if lineRemainder == "file" { - data, err := getCodeExampleForSummary(operation.Summary, operation.codeExampleFilesDir) + log.Println("line remainder is file") + + data, isJSON, err := getCodeExampleForSummary(operation.Summary, operation.codeExampleFilesDir) if err != nil { return err } var valueJSON interface{} - err = json.Unmarshal(data, &valueJSON) - if err != nil { - return fmt.Errorf("annotation %s need a valid json value", attribute) + if isJSON { + err = json.Unmarshal(data, &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) + } + } else { + err = yaml.Unmarshal(data, &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid yaml value. error: %s", attribute, err.Error()) + } } // don't use the method provided by spec lib, because it will call toLower() on attribute names, which is wrongly @@ -206,7 +219,7 @@ func (operation *Operation) ParseMetadata(attribute, lowerAttribute, lineRemaind err := json.Unmarshal([]byte(lineRemainder), &valueJSON) if err != nil { - return fmt.Errorf("annotation %s need a valid json value", attribute) + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) } // don't use the method provided by spec lib, because it will call toLower() on attribute names, which is wrongly @@ -1189,10 +1202,10 @@ func createParameter(paramType, description, paramName, objectType, schemaType s return result } -func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error) { +func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, bool, error) { dirEntries, err := os.ReadDir(dirPath) if err != nil { - return nil, err + return nil, false, err } for _, entry := range dirEntries { @@ -1202,7 +1215,9 @@ func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error fileName := entry.Name() - if !strings.Contains(fileName, ".json") { + isJson := strings.Contains(fileName, ".json") + isYaml := strings.Contains(fileName, ".yaml") + if !isJson && !isYaml { continue } @@ -1211,12 +1226,12 @@ func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error commentInfo, err := os.ReadFile(fullPath) if err != nil { - return nil, fmt.Errorf("Failed to read code example file %s error: %s ", fullPath, err) + return nil, false, fmt.Errorf("Failed to read code example file %s error: %s ", fullPath, err) } - return commentInfo, nil + return commentInfo, isJson, nil } } - return nil, fmt.Errorf("unable to find code example file for tag %s in the given directory", summaryName) + return nil, false, fmt.Errorf("unable to find code example file for tag %s in the given directory", summaryName) } diff --git a/operation_test.go b/operation_test.go index e71bda4ee..87bcae9ce 100644 --- a/operation_test.go +++ b/operation_test.go @@ -9,6 +9,7 @@ import ( "github.com/go-openapi/spec" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseEmptyComment(t *testing.T) { @@ -2202,7 +2203,7 @@ func TestParseExtentions(t *testing.T) { operation := NewOperation(nil) err := operation.ParseComment(comment, nil) - assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a valid json value") + assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a valid json value. error: invalid character '}' after array element") } // OK @@ -2345,10 +2346,12 @@ func TestParseCodeSamples(t *testing.T) { operation.Summary = "example" err := operation.ParseComment(comment, nil) - assert.NoError(t, err, "no error should be thrown") - assert.Equal(t, operation.Summary, "example") - assert.Equal(t, operation.Extensions["x-codeSamples"], - map[string]interface{}{"lang": "JavaScript", "source": "console.log('Hello World');"}) + require.NoError(t, err, "no error should be thrown") + + assert.Equal(t, "example", operation.Summary) + assert.Equal(t, []interface{}([]interface{}{map[string]interface{}{"lang": "JavaScript", "source": "console.log('Hello World');"}}), + operation.Extensions["x-codeSamples"], + ) }) t.Run("With broken file sample", func(t *testing.T) { diff --git a/operationv3.go b/operationv3.go new file mode 100644 index 000000000..219171634 --- /dev/null +++ b/operationv3.go @@ -0,0 +1,993 @@ +package swag + +import ( + "encoding/json" + "fmt" + "go/ast" + "log" + "net/http" + "strconv" + "strings" + + "github.com/sv-tools/openapi/spec" + "gopkg.in/yaml.v2" +) + +// Operation describes a single API operation on a path. +// For more information: https://github.com/swaggo/swag#api-operation +type OperationV3 struct { + parser *Parser + codeExampleFilesDir string + spec.Operation + RouterProperties []RouteProperties +} + +// NewOperationV3 returns a new instance of OperationV3. +func NewOperationV3(parser *Parser, options ...func(*OperationV3)) *OperationV3 { + op := *spec.NewOperation().Spec + op.Responses = spec.NewResponses() + + operation := &OperationV3{ + parser: parser, + Operation: op, + } + + for _, option := range options { + option(operation) + } + + return operation +} + +// SetCodeExampleFilesDirectory sets the directory to search for codeExamples. +func SetCodeExampleFilesDirectoryV3(directoryPath string) func(*OperationV3) { + return func(o *OperationV3) { + o.codeExampleFilesDir = directoryPath + } +} + +// ParseComment parses comment for given comment string and returns error if error occurs. +func (o *OperationV3) ParseComment(comment string, astFile *ast.File) error { + commentLine := strings.TrimSpace(strings.TrimLeft(comment, "/")) + if len(commentLine) == 0 { + return nil + } + + fields := FieldsByAnySpace(commentLine, 2) + attribute := fields[0] + lowerAttribute := strings.ToLower(attribute) + var lineRemainder string + if len(fields) > 1 { + lineRemainder = fields[1] + } + switch lowerAttribute { + case descriptionAttr: + o.ParseDescriptionComment(lineRemainder) + case descriptionMarkdownAttr: + commentInfo, err := getMarkdownForTag(lineRemainder, o.parser.markdownFileDir) + if err != nil { + return err + } + + o.ParseDescriptionComment(string(commentInfo)) + case summaryAttr: + o.Summary = lineRemainder + case idAttr: + o.OperationID = lineRemainder + case tagsAttr: + o.ParseTagsComment(lineRemainder) + case acceptAttr: + return o.ParseAcceptComment(lineRemainder) + case produceAttr: + return o.ParseProduceComment(lineRemainder) + case paramAttr: + return o.ParseParamComment(lineRemainder, astFile) + case successAttr, failureAttr, responseAttr: + return o.ParseResponseComment(lineRemainder, astFile) + case headerAttr: + return o.ParseResponseHeaderComment(lineRemainder, astFile) + case routerAttr: + return o.ParseRouterComment(lineRemainder) + case securityAttr: + return o.ParseSecurityComment(lineRemainder) + case deprecatedAttr: + o.Deprecated = true + case xCodeSamplesAttr, xCodeSamplesAttrOriginal: + return o.ParseCodeSample(attribute, commentLine, lineRemainder) + default: + return o.ParseMetadata(attribute, lowerAttribute, lineRemainder) + } + + return nil +} + +// ParseDescriptionComment parses the description comment and sets it to the operation. +func (o *OperationV3) ParseDescriptionComment(lineRemainder string) { + if o.Description == "" { + o.Description = lineRemainder + + return + } + + o.Description += "\n" + lineRemainder +} + +// ParseMetadata godoc. +func (o *OperationV3) ParseMetadata(attribute, lowerAttribute, lineRemainder string) error { + // parsing specific meta data extensions + if strings.HasPrefix(lowerAttribute, "@x-") { + if len(lineRemainder) == 0 { + return fmt.Errorf("annotation %s need a value", attribute) + } + + var valueJSON any + + err := json.Unmarshal([]byte(lineRemainder), &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) + } + + o.Responses.Extensions[attribute[1:]] = valueJSON + return nil + } + + return nil +} + +// ParseTagsComment parses comment for given `tag` comment string. +func (o *OperationV3) ParseTagsComment(commentLine string) { + for _, tag := range strings.Split(commentLine, ",") { + o.Tags = append(o.Tags, strings.TrimSpace(tag)) + } +} + +// ParseAcceptComment parses comment for given `accept` comment string. +func (o *OperationV3) ParseAcceptComment(commentLine string) error { + const errMessage = "could not parse accept comment" + + // TODO this must be moved into another comment + // return parseMimeTypeList(commentLine, &o.RequestBody.Spec.Spec.Content, ) + // result, err := parseMimeTypeListV3(commentLine, "%v accept type can't be accepted") + // if err != nil { + // return errors.Wrap(err, errMessage) + // } + + // for _, value := range result { + // o.RequestBody.Spec.Spec.Content[value] = spec.NewMediaType() + // } + + return nil +} + +// ParseProduceComment parses comment for given `produce` comment string. +func (o *OperationV3) ParseProduceComment(commentLine string) error { + const errMessage = "could not parse produce comment" + // return parseMimeTypeList(commentLine, &o.Responses, "%v produce type can't be accepted") + + // result, err := parseMimeTypeListV3(commentLine, "%v accept type can't be accepted") + // if err != nil { + // return errors.Wrap(err, errMessage) + // } + + // for _, value := range result { + // o.Responses.Spec.Response + // } + + // TODO the format of the comment needs to be changed in order to work + // The produce can be different per response code, so the produce mimetype needs to be included in the response comment + + return nil +} + +// parseMimeTypeList parses a list of MIME Types for a comment like +// `produce` (`Content-Type:` response header) or +// `accept` (`Accept:` request header). +func parseMimeTypeListV3(mimeTypeList string, format string) ([]string, error) { + var result []string + for _, typeName := range strings.Split(mimeTypeList, ",") { + if mimeTypePattern.MatchString(typeName) { + result = append(result, typeName) + + continue + } + + aliasMimeType, ok := mimeTypeAliases[typeName] + if !ok { + return nil, fmt.Errorf(format, typeName) + } + + result = append(result, aliasMimeType) + } + + return result, nil +} + +// ParseParamComment parses params return []string of param properties +// E.g. @Param queryText formData string true "The email for login" +// +// [param name] [paramType] [data type] [is mandatory?] [Comment] +// +// E.g. @Param some_id path int true "Some ID". +func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) error { + matches := paramPattern.FindStringSubmatch(commentLine) + if len(matches) != 6 { + return fmt.Errorf("missing required param comment parameters \"%s\"", commentLine) + } + + name := matches[1] + paramType := matches[2] + refType := TransToValidSchemeType(matches[3]) + + // Detect refType + objectType := OBJECT + + if strings.HasPrefix(refType, "[]") { + objectType = ARRAY + refType = strings.TrimPrefix(refType, "[]") + refType = TransToValidSchemeType(refType) + } else if IsPrimitiveType(refType) || + paramType == "formData" && refType == "file" { + objectType = PRIMITIVE + } + + var enums []interface{} + if !IsPrimitiveType(refType) { + schema, _ := o.parser.getTypeSchemaV3(refType, astFile, false) + if schema != nil && schema.Spec != nil && schema.Spec.Enum != nil { + // schema.Spec.Type != ARRAY + fmt.Println(schema.Spec.Type) + + if objectType == OBJECT { + objectType = PRIMITIVE + } + refType = TransToValidSchemeType(schema.Spec.Type[0]) + enums = schema.Spec.Enum + } + } + + requiredText := strings.ToLower(matches[4]) + required := requiredText == "true" || requiredText == requiredLabel + description := matches[5] + + param := createParameterV3(paramType, description, name, objectType, refType, required, enums, o.parser.collectionFormatInQuery) + + switch paramType { + case "path", "header": + switch objectType { + case ARRAY: + if !IsPrimitiveType(refType) { + return fmt.Errorf("%s is not supported array type for %s", refType, paramType) + } + case OBJECT: + return fmt.Errorf("%s is not supported type for %s", refType, paramType) + } + case "query", "formData": + switch objectType { + case ARRAY: + if !IsPrimitiveType(refType) && !(refType == "file" && paramType == "formData") { + return fmt.Errorf("%s is not supported array type for %s", refType, paramType) + } + case PRIMITIVE: + break + case OBJECT: + schema, err := o.parser.getTypeSchemaV3(refType, astFile, false) + if err != nil { + return err + } + + if len(schema.Spec.Properties) == 0 { + return nil + } + + for name, item := range schema.Spec.Properties { + prop := item.Spec + if len(prop.Type) == 0 { + continue + } + + switch { + case prop.Type[0] == ARRAY && + prop.Items.Schema != nil && + len(prop.Items.Schema.Spec.Type) > 0 && + IsSimplePrimitiveType(prop.Items.Schema.Spec.Type[0]): + + param = createParameterV3(paramType, prop.Description, name, prop.Type[0], prop.Items.Schema.Spec.Type[0], findInSlice(schema.Spec.Required, name), enums, o.parser.collectionFormatInQuery) + + case IsSimplePrimitiveType(prop.Type[0]): + param = createParameterV3(paramType, prop.Description, name, PRIMITIVE, prop.Type[0], findInSlice(schema.Spec.Required, name), enums, o.parser.collectionFormatInQuery) + default: + o.parser.debug.Printf("skip field [%s] in %s is not supported type for %s", name, refType, paramType) + + continue + } + + param.Schema.Spec = prop + + listItem := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ + Spec: &spec.Extendable[spec.Parameter]{ + Spec: ¶m, + }, + } + + o.Operation.Parameters = append(o.Operation.Parameters, listItem) + } + + return nil + } + case "body": + if objectType == PRIMITIVE { + param.Schema = PrimitiveSchemaV3(refType) + } else { + schema, err := o.parseAPIObjectSchema(commentLine, objectType, refType, astFile) + if err != nil { + return err + } + + param.Schema = schema + } + default: + return fmt.Errorf("%s is not supported paramType", paramType) + } + + err := o.parseParamAttribute(commentLine, objectType, refType, ¶m) + if err != nil { + return err + } + + item := spec.NewRefOrSpec(nil, &spec.Extendable[spec.Parameter]{ + Spec: ¶m, + }) + + o.Operation.Parameters = append(o.Operation.Parameters, item) + + return nil +} + +func (o *OperationV3) parseParamAttribute(comment, objectType, schemaType string, param *spec.Parameter) error { + schemaType = TransToValidSchemeType(schemaType) + + for attrKey, re := range regexAttributes { + attr, err := findAttr(re, comment) + if err != nil { + continue + } + + switch attrKey { + case enumsTag: + err = setEnumParamV3(param, attr, objectType, schemaType) + case minimumTag, maximumTag: + err = setNumberParamV3(param, attrKey, schemaType, attr, comment) + case defaultTag: + err = setDefaultV3(param, schemaType, attr) + case minLengthTag, maxLengthTag: + err = setStringParamV3(param, attrKey, schemaType, attr, comment) + case formatTag: + param.Schema.Spec.Format = attr + case exampleTag: + err = setExampleV3(param, schemaType, attr) + case schemaExampleTag: + err = setSchemaExampleV3(param, schemaType, attr) + case extensionsTag: + param.Schema.Spec.Extensions = setExtensionParam(attr) + case collectionFormatTag: + err = setCollectionFormatParamV3(param, attrKey, objectType, attr, comment) + } + + if err != nil { + return err + } + } + + return nil +} + +func setCollectionFormatParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { + if schemaType == ARRAY { + param.Style = TransToValidCollectionFormatV3(attr, param.In) + return nil + } + + return fmt.Errorf("%s is attribute to set to an array. comment=%s got=%s", name, commentLine, schemaType) +} + +func setSchemaExampleV3(param *spec.Parameter, schemaType string, value string) error { + val, err := defineType(schemaType, value) + if err != nil { + return nil // Don't set a example value if it's not valid + } + // skip schema + if param.Schema == nil { + return nil + } + + switch v := val.(type) { + case string: + // replaces \r \n \t in example string values. + param.Schema.Spec.Example = strings.NewReplacer(`\r`, "\r", `\n`, "\n", `\t`, "\t").Replace(v) + default: + param.Schema.Spec.Example = val + } + + return nil +} + +func setExampleV3(param *spec.Parameter, schemaType string, value string) error { + val, err := defineType(schemaType, value) + if err != nil { + return nil // Don't set a example value if it's not valid + } + + param.Example = val + + return nil +} + +func setStringParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { + if schemaType != STRING { + return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) + } + + n, err := strconv.Atoi(attr) + if err != nil { + return fmt.Errorf("%s is allow only a number got=%s", name, attr) + } + + switch name { + case minLengthTag: + param.Schema.Spec.MinLength = &n + case maxLengthTag: + param.Schema.Spec.MaxLength = &n + } + + return nil +} + +func setDefaultV3(param *spec.Parameter, schemaType string, value string) error { + val, err := defineType(schemaType, value) + if err != nil { + return nil // Don't set a default value if it's not valid + } + + param.Schema.Spec.Default = val + + return nil +} + +func setEnumParamV3(param *spec.Parameter, attr, objectType, schemaType string) error { + for _, e := range strings.Split(attr, ",") { + e = strings.TrimSpace(e) + + value, err := defineType(schemaType, e) + if err != nil { + return err + } + + switch objectType { + case ARRAY: + param.Schema.Spec.Items.Schema.Spec.Enum = append(param.Schema.Spec.Items.Schema.Spec.Enum, value) + default: + param.Schema.Spec.Enum = append(param.Schema.Spec.Enum, value) + } + } + + return nil +} + +func setNumberParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { + switch schemaType { + case INTEGER, NUMBER: + n, err := strconv.Atoi(attr) + if err != nil { + return fmt.Errorf("maximum is allow only a number. comment=%s got=%s", commentLine, attr) + } + + switch name { + case minimumTag: + param.Schema.Spec.Minimum = &n + case maximumTag: + param.Schema.Spec.Maximum = &n + } + + return nil + default: + return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) + } +} + +func (o *OperationV3) parseAPIObjectSchema(commentLine, schemaType, refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { + if strings.HasSuffix(refType, ",") && strings.Contains(refType, "[") { + // regexp may have broken generic syntax. find closing bracket and add it back + allMatchesLenOffset := strings.Index(commentLine, refType) + len(refType) + lostPartEndIdx := strings.Index(commentLine[allMatchesLenOffset:], "]") + if lostPartEndIdx >= 0 { + refType += commentLine[allMatchesLenOffset : allMatchesLenOffset+lostPartEndIdx+1] + } + } + + switch schemaType { + case OBJECT: + if !strings.HasPrefix(refType, "[]") { + return o.parseObjectSchema(refType, astFile) + } + + refType = refType[2:] + + fallthrough + case ARRAY: + schema, err := o.parseObjectSchema(refType, astFile) + if err != nil { + return nil, err + } + + result := spec.NewSchemaSpec() + result.Spec.Type = spec.NewSingleOrArray("array") + result.Spec.Items = spec.NewBoolOrSchema(false, schema) //TODO: allowed? + return result, nil + + default: + return PrimitiveSchemaV3(schemaType), nil + } +} + +// ParseRouterComment parses comment for given `router` comment string. +func (o *OperationV3) ParseRouterComment(commentLine string) error { + matches := routerPattern.FindStringSubmatch(commentLine) + if len(matches) != 3 { + return fmt.Errorf("can not parse router comment \"%s\"", commentLine) + } + + signature := RouteProperties{ + Path: matches[1], + HTTPMethod: strings.ToUpper(matches[2]), + } + + if _, ok := allMethod[signature.HTTPMethod]; !ok { + return fmt.Errorf("invalid method: %s", signature.HTTPMethod) + } + + o.RouterProperties = append(o.RouterProperties, signature) + + return nil +} + +// createParameter returns swagger spec.Parameter for given paramType, description, paramName, schemaType, required. +func createParameterV3(in, description, paramName, objectType, schemaType string, required bool, enums []interface{}, collectionFormat string) spec.Parameter { + // //five possible parameter types. query, path, body, header, form + result := spec.Parameter{ + Description: description, + Required: required, + Name: paramName, + In: in, + Schema: spec.NewRefOrSpec(nil, &spec.Schema{}), + } + + if in == "body" { + return result + } + + switch objectType { + case ARRAY: + result.Schema.Spec.Type = spec.NewSingleOrArray(objectType) + result.Schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + result.Schema.Spec.Items.Schema.Spec.Type = spec.NewSingleOrArray(schemaType) + result.Schema.Spec.Enum = enums + case PRIMITIVE, OBJECT: + result.Schema.Spec.Type = spec.NewSingleOrArray(schemaType) + result.Schema.Spec.Enum = enums + } + + return result +} + +func (o *OperationV3) parseObjectSchema(refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { + return parseObjectSchemaV3(o.parser, refType, astFile) +} + +func parseObjectSchemaV3(parser *Parser, refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { + switch { + case refType == NIL: + return nil, nil + case refType == INTERFACE: + return PrimitiveSchemaV3(OBJECT), nil + case refType == ANY: + return PrimitiveSchemaV3(OBJECT), nil + case IsGolangPrimitiveType(refType): + refType = TransToValidSchemeType(refType) + + return PrimitiveSchemaV3(refType), nil + case IsPrimitiveType(refType): + return PrimitiveSchemaV3(refType), nil + case strings.HasPrefix(refType, "[]"): + schema, err := parseObjectSchemaV3(parser, refType[2:], astFile) + if err != nil { + return nil, err + } + + result := spec.NewSchemaSpec() + result.Spec.Type = spec.NewSingleOrArray("array") + result.Spec.Items = spec.NewBoolOrSchema(false, schema) + + return result, nil + case strings.HasPrefix(refType, "map["): + // ignore key type + idx := strings.Index(refType, "]") + if idx < 0 { + return nil, fmt.Errorf("invalid type: %s", refType) + } + + refType = refType[idx+1:] + if refType == INTERFACE || refType == ANY { + schema := &spec.Schema{} + schema.AdditionalProperties = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + schema.Type = spec.NewSingleOrArray(OBJECT) + refOrSpec := spec.NewRefOrSpec(nil, schema) + return refOrSpec, nil + } + + schema, err := parseObjectSchemaV3(parser, refType, astFile) + if err != nil { + return nil, err + } + + result := &spec.Schema{} + result.AdditionalProperties = spec.NewBoolOrSchema(false, schema) + result.Type = spec.NewSingleOrArray(OBJECT) + refOrSpec := spec.NewSchemaSpec() + refOrSpec.Spec = result + + return refOrSpec, nil + case strings.Contains(refType, "{"): + return parseCombinedObjectSchemaV3(parser, refType, astFile) + default: + if parser != nil { // checking refType has existing in 'TypeDefinitions' + schema, err := parser.getTypeSchemaV3(refType, astFile, true) + if err != nil { + return nil, err + } + + return schema, nil + } + + return spec.NewSchemaRef(spec.NewRef("#/components/schemas/" + refType)), nil + } +} + +// ParseResponseHeaderComment parses comment for given `response header` comment string. +func (o *OperationV3) ParseResponseHeaderComment(commentLine string, _ *ast.File) error { + matches := responsePattern.FindStringSubmatch(commentLine) + if len(matches) != 5 { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + header := newHeaderSpecV3(strings.Trim(matches[2], "{}"), strings.Trim(matches[4], "\"")) + + headerKey := strings.TrimSpace(matches[3]) + + if strings.EqualFold(matches[1], "all") { + if o.Responses.Spec.Default != nil { + o.Responses.Spec.Default.Spec.Spec.Headers[headerKey] = header + } + + if o.Responses.Spec.Response != nil { + for _, v := range o.Responses.Spec.Response { + v.Spec.Spec.Headers[headerKey] = header + + } + } + + return nil + } + + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, defaultTag) { + if o.Responses.Spec.Default != nil { + o.Responses.Spec.Default.Spec.Spec.Headers[headerKey] = header + } + + continue + } + + _, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + // TODO check condition + if o.Responses != nil && o.Responses.Spec != nil && o.Responses.Spec.Response != nil { + response, responseExist := o.Responses.Spec.Response[codeStr] + if responseExist { + response.Spec.Spec.Headers[headerKey] = header + o.Responses.Spec.Response[codeStr] = response + } + } + } + + return nil +} + +func newHeaderSpecV3(schemaType, description string) *spec.RefOrSpec[spec.Extendable[spec.Header]] { + result := spec.NewHeaderSpec() + result.Spec.Spec.Description = description + result.Spec.Spec.Schema = spec.NewSchemaSpec() + result.Spec.Spec.Schema.Spec.Type = spec.NewSingleOrArray(schemaType) + + return result +} + +// ParseResponseComment parses comment for given `response` comment string. +func (o *OperationV3) ParseResponseComment(commentLine string, astFile *ast.File) error { + matches := responsePattern.FindStringSubmatch(commentLine) + if len(matches) != 5 { + err := o.ParseEmptyResponseComment(commentLine) + if err != nil { + return o.ParseEmptyResponseOnly(commentLine) + } + + return err + } + + description := strings.Trim(matches[4], "\"") + + schema, err := o.parseAPIObjectSchema(commentLine, strings.Trim(matches[2], "{}"), strings.TrimSpace(matches[3]), astFile) + if err != nil { + return err + } + + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, defaultTag) { + response := o.DefaultResponse() + response.Description = description + + mimeType := "application/json" // TODO: set correct mimeType + setResponseSchema(response, mimeType, schema) + + continue + } + + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + if description == "" { + description = http.StatusText(code) + } + + response := spec.NewResponseSpec() + response.Spec.Spec.Description = description + + mimeType := "application/json" // TODO: set correct mimeType + setResponseSchema(response.Spec.Spec, mimeType, schema) + + o.AddResponse(codeStr, response) + } + + return nil +} + +// setResponseSchema sets response schema for given response. +func setResponseSchema(response *spec.Response, mimeType string, schema *spec.RefOrSpec[spec.Schema]) { + mediaType := spec.NewMediaType() + mediaType.Spec.Schema = schema + + if response.Content == nil { + response.Content = make(map[string]*spec.Extendable[spec.MediaType]) + } + + response.Content[mimeType] = mediaType +} + +// ParseEmptyResponseComment parse only comment out status code and description,eg: @Success 200 "it's ok". +func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error { + matches := emptyResponsePattern.FindStringSubmatch(commentLine) + if len(matches) != 3 { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + description := strings.Trim(matches[2], "\"") + + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, defaultTag) { + response := o.DefaultResponse() + response.Description = description + + continue + } + + _, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + o.AddResponse(codeStr, newResponseWithDescription(description)) + } + + return nil +} + +// DefaultResponse return the default response member pointer. +func (o *OperationV3) DefaultResponse() *spec.Response { + if o.Responses.Spec.Default == nil { + o.Responses.Spec.Default = spec.NewResponseSpec() + o.Responses.Spec.Default.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) + } + + if o.Responses.Spec.Default.Spec.Spec.Content == nil { + o.Responses.Spec.Default.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) + } + + return o.Responses.Spec.Default.Spec.Spec +} + +// AddResponse add a response for a code. +func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Extendable[spec.Response]]) { + if response.Spec.Spec.Headers == nil { + response.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) + } + + if o.Responses.Spec.Response == nil { + o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]]) + } + + o.Responses.Spec.Response[code] = response +} + +// ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200. +func (o *OperationV3) ParseEmptyResponseOnly(commentLine string) error { + for _, codeStr := range strings.Split(commentLine, ",") { + if strings.EqualFold(codeStr, defaultTag) { + _ = o.DefaultResponse() + + continue + } + + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + o.AddResponse(codeStr, newResponseWithDescription(http.StatusText(code))) + } + + return nil +} + +func newResponseWithDescription(description string) *spec.RefOrSpec[spec.Extendable[spec.Response]] { + response := spec.NewResponseSpec() + response.Spec.Spec.Description = description + return response +} + +func parseCombinedObjectSchemaV3(parser *Parser, refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { + matches := combinedPattern.FindStringSubmatch(refType) + if len(matches) != 3 { + return nil, fmt.Errorf("invalid type: %s", refType) + } + + schema, err := parseObjectSchemaV3(parser, matches[1], astFile) + if err != nil { + return nil, err + } + + fields, props := parseFields(matches[2]), map[string]*spec.RefOrSpec[spec.Schema]{} + + for _, field := range fields { + keyVal := strings.SplitN(field, "=", 2) + if len(keyVal) != 2 { + continue + } + + schema, err := parseObjectSchemaV3(parser, keyVal[1], astFile) + if err != nil { + return nil, err + } + + props[keyVal[0]] = schema + } + + if len(props) == 0 { + return schema, nil + } + + if schema.Ref == nil && + len(schema.Spec.Type) > 0 && + schema.Spec.Type[0] == OBJECT && + len(schema.Spec.Properties) == 0 && + schema.Spec.AdditionalProperties == nil { + schema.Spec.Properties = props + return schema, nil + } + + schemaRefPath := strings.Replace(schema.Ref.Ref, "#/components/schemas/", "", 1) + schemaSpec := parser.openAPI.Components.Spec.Schemas[schemaRefPath] + schemaSpec.Spec.JsonSchemaComposition.AllOf = make([]*spec.RefOrSpec[spec.Schema], len(props)) + + i := 0 + for name, prop := range props { + wrapperSpec := spec.NewSchemaSpec() + wrapperSpec.Spec = &spec.Schema{} + wrapperSpec.Spec.Type = spec.NewSingleOrArray(OBJECT) + wrapperSpec.Spec.Properties = map[string]*spec.RefOrSpec[spec.Schema]{ + name: prop, + } + + parser.openAPI.Components.Spec.Schemas[name] = wrapperSpec + + ref := spec.NewRefOrSpec[spec.Schema](spec.NewRef("#/components/schemas/"+name), nil) + + schemaSpec.Spec.JsonSchemaComposition.AllOf[i] = ref + i++ + } + + return schemaSpec, nil +} + +// ParseSecurityComment parses comment for given `security` comment string. +func (o *OperationV3) ParseSecurityComment(commentLine string) error { + var ( + securityMap = make(map[string][]string) + securitySource = commentLine[strings.Index(commentLine, "@Security")+1:] + ) + + for _, securityOption := range strings.Split(securitySource, "||") { + securityOption = strings.TrimSpace(securityOption) + + left, right := strings.Index(securityOption, "["), strings.Index(securityOption, "]") + + if !(left == -1 && right == -1) { + scopes := securityOption[left+1 : right] + + var options []string + + for _, scope := range strings.Split(scopes, ",") { + options = append(options, strings.TrimSpace(scope)) + } + + securityKey := securityOption[0:left] + securityMap[securityKey] = append(securityMap[securityKey], options...) + } else { + securityKey := strings.TrimSpace(securityOption) + securityMap[securityKey] = []string{} + } + } + + o.Security = append(o.Security, securityMap) + + return nil +} + +// ParseCodeSample godoc. +func (o *OperationV3) ParseCodeSample(attribute, _, lineRemainder string) error { + log.Println("line remainder:", lineRemainder) + + if lineRemainder == "file" { + log.Println("line remainder is file") + + data, isJSON, err := getCodeExampleForSummary(o.Summary, o.codeExampleFilesDir) + if err != nil { + return err + } + + // using custom type, as json marshaller has problems with []map[interface{}]map[interface{}]interface{} + var valueJSON CodeSamples + + if isJSON { + err = json.Unmarshal(data, &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) + } + } else { + err = yaml.Unmarshal(data, &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid yaml value. error: %s", attribute, err.Error()) + } + } + + o.Responses.Extensions[attribute[1:]] = valueJSON + + return nil + } + + // Fallback into existing logic + return o.ParseMetadata(attribute, strings.ToLower(attribute), lineRemainder) +} diff --git a/operationv3_test.go b/operationv3_test.go new file mode 100644 index 000000000..5b552caca --- /dev/null +++ b/operationv3_test.go @@ -0,0 +1,1916 @@ +package swag + +import ( + goparser "go/parser" + "go/token" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/sv-tools/openapi/spec" +) + +var typeObject = spec.SingleOrArray[string](spec.SingleOrArray[string]{OBJECT}) +var typeArray = spec.SingleOrArray[string](spec.SingleOrArray[string]{ARRAY}) +var typeInteger = spec.SingleOrArray[string](spec.SingleOrArray[string]{INTEGER}) +var typeString = spec.SingleOrArray[string](spec.SingleOrArray[string]{STRING}) +var typeFile = spec.SingleOrArray[string](spec.SingleOrArray[string]{"file"}) +var typeNumber = spec.SingleOrArray[string](spec.SingleOrArray[string]{NUMBER}) +var typeBool = spec.SingleOrArray[string](spec.SingleOrArray[string]{BOOLEAN}) + +func TestParseEmptyCommentV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + err := operation.ParseComment("//", nil) + + require.NoError(t, err) +} + +func TestParseTagsCommentV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + err := operation.ParseComment(`/@Tags pet, store,user`, nil) + require.NoError(t, err) + assert.Equal(t, operation.Tags, []string{"pet", "store", "user"}) +} + +func TestParseRouterCommentV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{wishlist_id} [get]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + assert.Len(t, operation.RouterProperties, 1) + assert.Equal(t, "/customer/get-wishlist/{wishlist_id}", operation.RouterProperties[0].Path) + assert.Equal(t, "GET", operation.RouterProperties[0].HTTPMethod) +} + +func TestParseRouterMultipleCommentsV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{wishlist_id} [get]` + anotherComment := `/@Router /customer/get-the-wishlist/{wishlist_id} [post]` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + err = operation.ParseComment(anotherComment, nil) + require.NoError(t, err) + + assert.Len(t, operation.RouterProperties, 2) + assert.Equal(t, "/customer/get-wishlist/{wishlist_id}", operation.RouterProperties[0].Path) + assert.Equal(t, "GET", operation.RouterProperties[0].HTTPMethod) + assert.Equal(t, "/customer/get-the-wishlist/{wishlist_id}", operation.RouterProperties[1].Path) + assert.Equal(t, "POST", operation.RouterProperties[1].HTTPMethod) +} + +func TestParseRouterOnlySlashV3(t *testing.T) { + t.Parallel() + + comment := `// @Router / [get]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + assert.Len(t, operation.RouterProperties, 1) + assert.Equal(t, "/", operation.RouterProperties[0].Path) + assert.Equal(t, "GET", operation.RouterProperties[0].HTTPMethod) +} + +func TestParseRouterCommentWithPlusSignV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{proxy+} [post]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + assert.Len(t, operation.RouterProperties, 1) + assert.Equal(t, "/customer/get-wishlist/{proxy+}", operation.RouterProperties[0].Path) + assert.Equal(t, "POST", operation.RouterProperties[0].HTTPMethod) +} + +func TestParseRouterCommentWithDollarSignV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{wishlist_id}$move [post]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + assert.Len(t, operation.RouterProperties, 1) + assert.Equal(t, "/customer/get-wishlist/{wishlist_id}$move", operation.RouterProperties[0].Path) + assert.Equal(t, "POST", operation.RouterProperties[0].HTTPMethod) +} + +func TestParseRouterCommentNoDollarSignAtPathStartErrV3(t *testing.T) { + t.Parallel() + + comment := `/@Router $customer/get-wishlist/{wishlist_id}$move [post]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseRouterCommentWithColonSignV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{wishlist_id}:move [post]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + assert.Len(t, operation.RouterProperties, 1) + assert.Equal(t, "/customer/get-wishlist/{wishlist_id}:move", operation.RouterProperties[0].Path) + assert.Equal(t, "POST", operation.RouterProperties[0].HTTPMethod) +} + +func TestParseRouterCommentNoColonSignAtPathStartErrV3(t *testing.T) { + t.Parallel() + + comment := `/@Router :customer/get-wishlist/{wishlist_id}:move [post]` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseRouterCommentMethodSeparationErrV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /api/{id}|,*[get` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseRouterCommentMethodMissingErrV3(t *testing.T) { + t.Parallel() + + comment := `/@Router /customer/get-wishlist/{wishlist_id}` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestOperation_ParseResponseWithDefaultV3(t *testing.T) { + t.Parallel() + + comment := `@Success default {object} nil "An empty response"` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + assert.Equal(t, "An empty response", operation.Responses.Spec.Default.Spec.Spec.Description) + + comment = `@Success 200,default {string} Response "A response"` + operation = NewOperationV3(nil) + + err = operation.ParseComment(comment, nil) + require.NoError(t, err) + + assert.Equal(t, "A response", operation.Responses.Spec.Default.Spec.Spec.Description) + assert.Equal(t, "A response", operation.Responses.Spec.Response["200"].Spec.Spec.Description) +} + +func TestParseResponseSuccessCommentWithEmptyResponseV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} nil "An empty response"` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `An empty response`, response.Spec.Spec.Description) +} + +func TestParseResponseFailureCommentWithEmptyResponseV3(t *testing.T) { + t.Parallel() + + comment := `@Failure 500 {object} nil` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + assert.Equal(t, "Internal Server Error", operation.Responses.Spec.Response["500"].Spec.Spec.Description) +} + +func TestParseResponseCommentWithObjectTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.OrderRow "Error message, if code != 200` + parser := New() + operation := NewOperationV3(parser) + operation.parser.addTestType("model.OrderRow") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + + assert.Equal(t, "#/components/schemas/model.OrderRow", response.Spec.Spec.Content["application/json"].Spec.Schema.Ref.Ref) +} + +func TestParseResponseCommentWithNestedPrimitiveTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data=string,data2=int} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + require.NotNil(t, response.Spec.Spec.Content["application/json"].Spec.Schema) + + allOf := operation.Responses.Spec.Response["200"].Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf + require.NotNil(t, allOf) + assert.Equal(t, 2, len(allOf)) + assert.Equal(t, "#/components/schemas/data", allOf[0].Ref.Ref) + assert.Equal(t, "#/components/schemas/data2", allOf[1].Ref.Ref) +} + +func TestParseResponseCommentWithNestedPrimitiveArrayTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data=[]string,data2=[]int} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + assert.NotNil(t, operation.parser.openAPI.Components.Spec.Schemas["data"].Spec.Properties["data"]) + assert.Equal(t, spec.SingleOrArray[string](spec.SingleOrArray[string]{"string"}), operation.parser.openAPI.Components.Spec.Schemas["data"].Spec.Properties["data"].Spec.Items.Schema.Spec.Type) +} + +func TestParseResponseCommentWithNestedObjectTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data=model.Payload,data2=model.Payload2} "Error message, if code != 200` + operation := NewOperationV3(New()) + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.Payload2") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + assert.Equal(t, 2, len(response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf)) + assert.Equal(t, 5, len(operation.parser.openAPI.Components.Spec.Schemas)) + + assert.Equal(t, "#/components/schemas/model.Payload", operation.parser.openAPI.Components.Spec.Schemas["data"].Spec.Properties["data"].Ref.Ref) + assert.Equal(t, "#/components/schemas/model.Payload2", operation.parser.openAPI.Components.Spec.Schemas["data2"].Spec.Properties["data2"].Ref.Ref) +} + +func TestParseResponseCommentWithNestedArrayObjectTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data=[]model.Payload,data2=[]model.Payload2} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.Payload2") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + + allOf := response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf + assert.Equal(t, 2, len(allOf)) + + assert.Equal(t, "#/components/schemas/model.Payload", operation.parser.openAPI.Components.Spec.Schemas["data"].Spec.Properties["data"].Spec.Items.Schema.Ref.Ref) + assert.Equal(t, typeArray, operation.parser.openAPI.Components.Spec.Schemas["data"].Spec.Properties["data"].Spec.Type) + + assert.Equal(t, "#/components/schemas/model.Payload2", operation.parser.openAPI.Components.Spec.Schemas["data2"].Spec.Properties["data2"].Spec.Items.Schema.Ref.Ref) + assert.Equal(t, typeArray, operation.parser.openAPI.Components.Spec.Schemas["data2"].Spec.Properties["data2"].Spec.Type) +} + +func TestParseResponseCommentWithNestedFieldsV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data1=int,data2=[]int,data3=model.Payload,data4=[]model.Payload} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + + allOf := response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf + assert.Equal(t, 4, len(allOf)) + + schemas := operation.parser.openAPI.Components.Spec.Schemas + + assert.Equal(t, typeInteger, schemas["data1"].Spec.Properties["data1"].Spec.Type) + assert.Equal(t, typeObject, schemas["data1"].Spec.Type) + + assert.Equal(t, typeArray, schemas["data2"].Spec.Properties["data2"].Spec.Type) + assert.Equal(t, typeInteger, schemas["data2"].Spec.Properties["data2"].Spec.Items.Schema.Spec.Type) + assert.Equal(t, typeObject, schemas["data2"].Spec.Type) + + assert.Equal(t, "#/components/schemas/model.Payload", schemas["data3"].Spec.Properties["data3"].Ref.Ref) + assert.Equal(t, typeObject, schemas["data3"].Spec.Type) + + assert.Equal(t, "#/components/schemas/model.Payload", schemas["data4"].Spec.Properties["data4"].Spec.Items.Schema.Ref.Ref) + assert.Equal(t, typeArray, schemas["data4"].Spec.Properties["data4"].Spec.Type) + assert.Equal(t, typeObject, schemas["data4"].Spec.Type) +} + +func TestParseResponseCommentWithDeepNestedFieldsV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.CommonHeader{data1=int,data2=[]int,data3=model.Payload{data1=int,data2=model.DeepPayload},data4=[]model.Payload{data1=[]int,data2=[]model.DeepPayload}} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.DeepPayload") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + + allOf := response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf + assert.Equal(t, 4, len(allOf)) + + schemas := operation.parser.openAPI.Components.Spec.Schemas + + assert.Equal(t, typeInteger, schemas["data1"].Spec.Properties["data1"].Spec.Type) + assert.Equal(t, typeObject, schemas["data1"].Spec.Type) + + assert.Equal(t, typeArray, schemas["data2"].Spec.Properties["data2"].Spec.Type) + assert.Equal(t, typeInteger, schemas["data2"].Spec.Properties["data2"].Spec.Items.Schema.Spec.Type) + assert.Equal(t, typeObject, schemas["data2"].Spec.Type) + + assert.Equal(t, typeObject, schemas["data3"].Spec.Type) + assert.Equal(t, typeObject, schemas["data3"].Spec.Properties["data3"].Spec.Type) + assert.Equal(t, 2, len(schemas["data3"].Spec.Properties["data3"].Spec.AllOf)) + + assert.Equal(t, typeObject, schemas["data4"].Spec.Type) + assert.Equal(t, typeArray, schemas["data4"].Spec.Properties["data4"].Spec.Type) + assert.Equal(t, typeObject, schemas["data4"].Spec.Properties["data4"].Spec.Items.Schema.Spec.Type) + assert.Equal(t, 2, len(schemas["data4"].Spec.Properties["data4"].Spec.Items.Schema.Spec.AllOf)) +} + +func TestParseResponseCommentWithNestedArrayMapFieldsV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} []map[string]model.CommonHeader{data1=[]map[string]model.Payload,data2=map[string][]int} "Error message, if code != 200` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + + content := response.Spec.Spec.Content["application/json"] + assert.NotNil(t, content) + assert.NotNil(t, content.Spec) + assert.NotNil(t, content.Spec.Schema.Spec.Items.Schema.Spec.AdditionalProperties.Schema) + + assert.Equal(t, 2, len(content.Spec.Schema.Spec.Items.Schema.Spec.AdditionalProperties.Schema.Spec.AllOf)) + assert.Equal(t, typeArray, content.Spec.Schema.Spec.Type) + assert.Equal(t, typeObject, content.Spec.Schema.Spec.Items.Schema.Spec.Type) + assert.Equal(t, typeObject, content.Spec.Schema.Spec.Items.Schema.Spec.AdditionalProperties.Schema.Spec.Type) + + schemas := operation.parser.openAPI.Components.Spec.Schemas + + data1 := schemas["data1"] + assert.NotNil(t, data1) + assert.NotNil(t, data1.Spec) + assert.NotNil(t, data1.Spec.Properties) + + assert.Equal(t, typeObject, data1.Spec.Type) + assert.Equal(t, typeArray, data1.Spec.Properties["data1"].Spec.Type) + assert.Equal(t, typeObject, data1.Spec.Properties["data1"].Spec.Items.Schema.Spec.Type) + assert.Equal(t, "#/components/schemas/model.Payload", data1.Spec.Properties["data1"].Spec.Items.Schema.Spec.AdditionalProperties.Schema.Ref.Ref) + + data2 := schemas["data2"] + assert.NotNil(t, data2) + assert.NotNil(t, data2.Spec) + assert.NotNil(t, data2.Spec.Properties) + + assert.Equal(t, typeObject, data2.Spec.Type) + assert.Equal(t, typeObject, data2.Spec.Properties["data2"].Spec.Type) + assert.Equal(t, typeArray, data2.Spec.Properties["data2"].Spec.AdditionalProperties.Schema.Spec.Type) + assert.Equal(t, typeInteger, data2.Spec.Properties["data2"].Spec.AdditionalProperties.Schema.Spec.Items.Schema.Spec.Type) + + commonHeader := schemas["model.CommonHeader"] + assert.NotNil(t, commonHeader) + assert.NotNil(t, commonHeader.Spec) + assert.Equal(t, 2, len(commonHeader.Spec.AllOf)) + assert.Equal(t, typeObject, commonHeader.Spec.Type) + + payload := schemas["model.Payload"] + assert.NotNil(t, payload) + assert.NotNil(t, payload.Spec) + assert.Equal(t, typeObject, payload.Spec.Type) +} + +func TestParseResponseCommentWithObjectTypeInSameFileV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} testOwner "Error message, if code != 200"` + operation := NewOperationV3(New()) + + operation.parser.addTestType("swag.testOwner") + + fset := token.NewFileSet() + astFile, err := goparser.ParseFile(fset, "operation_test.go", `package swag + type testOwner struct { + + } + `, goparser.ParseComments) + assert.NoError(t, err) + + err = operation.ParseComment(comment, astFile) + assert.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + assert.Equal(t, "#/components/schemas/swag.testOwner", response.Spec.Spec.Content["application/json"].Spec.Schema.Ref.Ref) +} + +func TestParseResponseCommentWithObjectTypeErrV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {object} model.OrderRow "Error message, if code != 200"` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.notexist") + + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseResponseCommentWithArrayTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {array} model.OrderRow "Error message, if code != 200` + operation := NewOperationV3(New()) + operation.parser.addTestType("model.OrderRow") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + response := operation.Responses.Spec.Response["200"] + assert.Equal(t, `Error message, if code != 200`, response.Spec.Spec.Description) + assert.Equal(t, typeArray, response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) + assert.Equal(t, "#/components/schemas/model.OrderRow", response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Items.Schema.Ref.Ref) + +} + +func TestParseResponseCommentWithBasicTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 {string} string "it's ok'"` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + require.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok'", response.Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) +} + +func TestParseResponseCommentWithBasicTypeAndCodesV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200,201,default {string} string "it's ok"` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Response["201"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Default + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) +} + +func TestParseEmptyResponseCommentV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200 "it is ok"` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it is ok", response.Spec.Spec.Description) +} + +func TestParseEmptyResponseCommentWithCodesV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200,201,default "it is ok"` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it is ok", response.Spec.Spec.Description) + + response = operation.Responses.Spec.Response["201"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it is ok", response.Spec.Spec.Description) + + response = operation.Responses.Spec.Default + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it is ok", response.Spec.Spec.Description) +} + +func TestParseResponseCommentWithHeaderV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + err := operation.ParseComment(`@Success 200 "it's ok"`, nil) + assert.NoError(t, err, "ParseComment should not fail") + + err = operation.ParseComment(`@Header 200 {string} Token "qwerty"`, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + err = operation.ParseComment(`@Header 200 "Mallformed"`, nil) + assert.Error(t, err, "ParseComment should fail") + + err = operation.ParseComment(`@Header 200,asdsd {string} Token "qwerty"`, nil) + assert.Error(t, err, "ParseComment should fail") +} + +func TestParseResponseCommentWithHeaderForCodesV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + + comment := `@Success 200,201,default "it's ok"` + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + comment = `@Header 200,201,default {string} Token "qwerty"` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + comment = `@Header all {string} Token2 "qwerty"` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token2"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token2"].Spec.Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Response["201"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token2"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token2"].Spec.Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Default + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token2"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token2"].Spec.Spec.Schema.Spec.Type) + + comment = `@Header 200 "Mallformed"` + err = operation.ParseComment(comment, nil) + assert.Error(t, err, "ParseComment should not fail") +} + +func TestParseResponseCommentWithHeaderOnlyAllV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + + comment := `@Success 200,201,default "it's ok"` + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + comment = `@Header all {string} Token "qwerty"` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Response["201"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + response = operation.Responses.Spec.Default + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "it's ok", response.Spec.Spec.Description) + + assert.Equal(t, "qwerty", response.Spec.Spec.Headers["Token"].Spec.Spec.Description) + assert.Equal(t, typeString, response.Spec.Spec.Headers["Token"].Spec.Spec.Schema.Spec.Type) + + comment = `@Header 200 "Mallformed"` + err = operation.ParseComment(comment, nil) + assert.Error(t, err, "ParseComment should not fail") +} + +func TestParseEmptyResponseOnlyCodeV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + err := operation.ParseComment(`@Success 200`, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "OK", response.Spec.Spec.Description) +} + +func TestParseEmptyResponseOnlyCodesV3(t *testing.T) { + t.Parallel() + + comment := `@Success 200,201,default` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + response := operation.Responses.Spec.Response["200"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "OK", response.Spec.Spec.Description) + + response = operation.Responses.Spec.Response["201"] + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "Created", response.Spec.Spec.Description) + + response = operation.Responses.Spec.Default + assert.NotNil(t, response) + assert.NotNil(t, response.Spec) + + assert.Equal(t, "", response.Spec.Spec.Description) +} + +func TestParseResponseCommentParamMissingV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(nil) + + paramLenErrComment := `@Success notIntCode` + paramLenErr := operation.ParseComment(paramLenErrComment, nil) + assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode"`) + + paramLenErrComment = `@Success notIntCode {string} string "it ok"` + paramLenErr = operation.ParseComment(paramLenErrComment, nil) + assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode {string} string "it ok""`) + + paramLenErrComment = `@Success notIntCode "it ok"` + paramLenErr = operation.ParseComment(paramLenErrComment, nil) + assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode "it ok""`) +} + +func TestOperation_ParseParamCommentV3(t *testing.T) { + t.Parallel() + + t.Run("integer", func(t *testing.T) { + t.Parallel() + for _, paramType := range []string{"header", "path", "query", "formData"} { + t.Run(paramType, func(t *testing.T) { + o := NewOperationV3(New()) + err := o.ParseComment(`@Param some_id `+paramType+` int true "Some ID"`, nil) + + assert.NoError(t, err) + + expected := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ + Spec: &spec.Extendable[spec.Parameter]{ + Spec: &spec.Parameter{ + Name: "some_id", + Description: "Some ID", + In: paramType, + Required: true, + Schema: &spec.RefOrSpec[spec.Schema]{ + Spec: &spec.Schema{ + JsonSchema: spec.JsonSchema{ + JsonSchemaCore: spec.JsonSchemaCore{ + Type: typeInteger, + }, + }, + }, + }, + }, + }, + } + + expectedArray := []*spec.RefOrSpec[spec.Extendable[spec.Parameter]]{expected} + assert.Equal(t, o.Parameters, expectedArray) + }) + } + }) + + t.Run("string", func(t *testing.T) { + t.Parallel() + for _, paramType := range []string{"header", "path", "query", "formData"} { + t.Run(paramType, func(t *testing.T) { + o := NewOperationV3(New()) + err := o.ParseComment(`@Param some_string `+paramType+` string true "Some String"`, nil) + + assert.NoError(t, err) + expected := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ + Spec: &spec.Extendable[spec.Parameter]{ + Spec: &spec.Parameter{ + Description: "Some String", + Name: "some_string", + In: paramType, + Required: true, + Schema: &spec.RefOrSpec[spec.Schema]{ + Spec: &spec.Schema{ + JsonSchema: spec.JsonSchema{ + JsonSchemaCore: spec.JsonSchemaCore{ + Type: typeString, + }, + }, + }, + }, + }, + }, + } + + expectedArray := []*spec.RefOrSpec[spec.Extendable[spec.Parameter]]{expected} + assert.Equal(t, o.Parameters, expectedArray) + }) + } + }) + + t.Run("object", func(t *testing.T) { + t.Parallel() + for _, paramType := range []string{"header", "path", "query", "formData"} { + t.Run(paramType, func(t *testing.T) { + assert.Error(t, + NewOperationV3(New()). + ParseComment(`@Param some_object `+paramType+` main.Object true "Some Object"`, + nil)) + }) + } + }) + +} + +// Test ParseParamComment Query Params +func TestParseParamCommentBodyArrayV3(t *testing.T) { + t.Parallel() + + comment := `@Param names body []string true "Users List"` + o := NewOperationV3(New()) + err := o.ParseComment(comment, nil) + assert.NoError(t, err) + + expected := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ + Spec: &spec.Extendable[spec.Parameter]{ + Spec: &spec.Parameter{ + Name: "names", + Description: "Users List", + In: "body", + Required: true, + Schema: &spec.RefOrSpec[spec.Schema]{ + Spec: &spec.Schema{ + JsonSchema: spec.JsonSchema{ + JsonSchemaCore: spec.JsonSchemaCore{ + Type: typeArray, + }, + JsonSchemaTypeArray: spec.JsonSchemaTypeArray{ + Items: &spec.BoolOrSchema{ + Schema: &spec.RefOrSpec[spec.Schema]{ + Spec: &spec.Schema{ + JsonSchema: spec.JsonSchema{ + JsonSchemaCore: spec.JsonSchemaCore{ + Type: typeString, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + expectedArray := []*spec.RefOrSpec[spec.Extendable[spec.Parameter]]{expected} + assert.Equal(t, o.Parameters, expectedArray) + +} + +func TestParseParamCommentArrayV3(t *testing.T) { + paramTypes := []string{"header", "path", "query"} + + for _, paramType := range paramTypes { + t.Run(paramType, func(t *testing.T) { + operation := NewOperationV3(New()) + err := operation.ParseComment(`@Param names `+paramType+` []string true "Users List"`, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Users List", parameterSpec.Description) + assert.Equal(t, "names", parameterSpec.Name) + assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, paramType, parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Items.Schema.Spec.Type) + + err = operation.ParseComment(`@Param names `+paramType+` []model.User true "Users List"`, nil) + assert.Error(t, err) + }) + } +} + +func TestParseParamCommentDefaultValueV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(New()) + err := operation.ParseComment(`@Param names query string true "Users List" default(test)`, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Users List", parameterSpec.Description) + assert.Equal(t, "names", parameterSpec.Name) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, "test", parameterSpec.Schema.Spec.Default) +} + +func TestParseParamCommentQueryArrayFormatV3(t *testing.T) { + t.Parallel() + + comment := `@Param names query []string true "Users List" collectionFormat(multi)` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Users List", parameterSpec.Description) + assert.Equal(t, "names", parameterSpec.Name) + assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Items.Schema.Spec.Type) + assert.Equal(t, "form", parameterSpec.Style) + +} + +func TestParseParamCommentByIDV3(t *testing.T) { + t.Parallel() + + comment := `@Param unsafe_id[lte] query int true "Unsafe query param"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Unsafe query param", parameterSpec.Description) + assert.Equal(t, "unsafe_id[lte]", parameterSpec.Name) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) +} + +func TestParseParamCommentByQueryTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query int true "Some ID"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) +} + +func TestParseParamCommentByBodyTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id body model.OrderRow true "Some ID"` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.OrderRow") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, "#/components/schemas/model.OrderRow", parameterSpec.Schema.Ref.Ref) +} + +func TestParseParamCommentByBodyTextPlainV3(t *testing.T) { + t.Parallel() + + comment := `@Param text body string true "Text to process"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Text to process", parameterSpec.Description) + assert.Equal(t, "text", parameterSpec.Name) + assert.Equal(t, true, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) +} + +func TestParseParamCommentByBodyTypeWithDeepNestedFieldsV3(t *testing.T) { + t.Parallel() + + comment := `@Param body body model.CommonHeader{data=string,data2=int} true "test deep"` + operation := NewOperationV3(New()) + + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Len(t, operation.Parameters, 1) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "test deep", parameterSpec.Description) + assert.Equal(t, "body", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + + assert.Equal(t, 2, len(parameterSpec.Schema.Spec.AllOf)) + assert.Equal(t, 3, len(operation.parser.openAPI.Components.Spec.Schemas)) +} + +func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id body []int true "Some ID"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Items.Schema.Spec.Type) +} + +func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoWithDeepNestedFieldsV3(t *testing.T) { + t.Parallel() + + comment := `@Param body body []model.CommonHeader{data=string,data2=int} true "test deep"` + operation := NewOperationV3(New()) + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Len(t, operation.Parameters, 1) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "test deep", parameterSpec.Description) + assert.Equal(t, "body", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 2, len(parameterSpec.Schema.Spec.Items.Schema.Spec.AllOf)) +} + +func TestParseParamCommentByBodyTypeErrV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id body model.OrderRow true "Some ID"` + operation := NewOperationV3(New()) + operation.parser.addTestType("model.notexist") + + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseParamCommentByFormDataTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Param file formData file true "this is a test file"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Len(t, operation.Parameters, 1) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "this is a test file", parameterSpec.Description) + assert.Equal(t, "file", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "formData", parameterSpec.In) + assert.Equal(t, typeFile, parameterSpec.Schema.Spec.Type) +} + +func TestParseParamCommentByFormDataTypeUint64V3(t *testing.T) { + t.Parallel() + + comment := `@Param file formData uint64 true "this is a test file"` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Len(t, operation.Parameters, 1) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "this is a test file", parameterSpec.Description) + assert.Equal(t, "file", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "formData", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) +} + +func TestParseParamCommentByNotSupportedTypeV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id not_supported int true "Some ID"` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + + assert.Error(t, err) +} + +func TestParseParamCommentNotMatchV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id body mock true` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + + assert.Error(t, err) +} + +func TestParseParamCommentByEnumsV3(t *testing.T) { + t.Parallel() + + t.Run("string", func(t *testing.T) { + comment := `@Param some_id query string true "Some ID" Enums(A, B, C)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Len(t, operation.Parameters, 1) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 3, len(parameterSpec.Schema.Spec.Enum)) + + enums := []interface{}{"A", "B", "C"} + assert.EqualValues(t, enums, parameterSpec.Schema.Spec.Enum) + }) + + t.Run("int", func(t *testing.T) { + comment := `@Param some_id query int true "Some ID" Enums(1, 2, 3)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 3, len(parameterSpec.Schema.Spec.Enum)) + + enums := []interface{}{1, 2, 3} + assert.EqualValues(t, enums, parameterSpec.Schema.Spec.Enum) + }) + + t.Run("number", func(t *testing.T) { + comment := `@Param some_id query number true "Some ID" Enums(1.1, 2.2, 3.3)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeNumber, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 3, len(parameterSpec.Schema.Spec.Enum)) + + enums := []interface{}{1.1, 2.2, 3.3} + assert.EqualValues(t, enums, parameterSpec.Schema.Spec.Enum) + }) + + t.Run("bool", func(t *testing.T) { + comment := `@Param some_id query bool true "Some ID" Enums(true, false)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeBool, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 2, len(parameterSpec.Schema.Spec.Enum)) + + enums := []interface{}{true, false} + assert.EqualValues(t, enums, parameterSpec.Schema.Spec.Enum) + }) + + operation := NewOperationV3(New()) + + comment := `@Param some_id query int true "Some ID" Enums(A, B, C)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query number true "Some ID" Enums(A, B, C)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query boolean true "Some ID" Enums(A, B, C)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query Document true "Some ID" Enums(A, B, C)` + assert.Error(t, operation.ParseComment(comment, nil)) +} + +func TestParseParamCommentByMaxLengthV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query string true "Some ID" MaxLength(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, *parameterSpec.Schema.Spec.MaxLength) + + comment = `@Param some_id query int true "Some ID" MaxLength(10)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query string true "Some ID" MaxLength(Goopher)` + assert.Error(t, operation.ParseComment(comment, nil)) +} + +func TestParseParamCommentByMinLengthV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query string true "Some ID" MinLength(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, *parameterSpec.Schema.Spec.MinLength) + + comment = `@Param some_id query int true "Some ID" MinLength(10)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query string true "Some ID" MinLength(Goopher)` + assert.Error(t, operation.ParseComment(comment, nil)) +} + +func TestParseParamCommentByMinimumV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query int true "Some ID" Minimum(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, *parameterSpec.Schema.Spec.Minimum) + + comment = `@Param some_id query int true "Some ID" Mininum(10)` + assert.NoError(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query string true "Some ID" Minimum(10)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query integer true "Some ID" Minimum(Goopher)` + assert.Error(t, operation.ParseComment(comment, nil)) +} + +func TestParseParamCommentByMaximumV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query int true "Some ID" Maximum(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, *parameterSpec.Schema.Spec.Maximum) + + comment = `@Param some_id query int true "Some ID" Maxinum(10)` + assert.NoError(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query string true "Some ID" Maximum(10)` + assert.Error(t, operation.ParseComment(comment, nil)) + + comment = `@Param some_id query integer true "Some ID" Maximum(Goopher)` + assert.Error(t, operation.ParseComment(comment, nil)) +} + +func TestParseParamCommentByDefaultV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query int true "Some ID" Default(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, parameterSpec.Schema.Spec.Default) +} + +func TestParseParamCommentByExampleIntV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query int true "Some ID" Example(10)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, 10, parameterSpec.Example) +} + +func TestParseParamCommentByExampleStringV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id query string true "Some ID" Example(True feelings)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + assert.Equal(t, "True feelings", parameterSpec.Example) +} + +func TestParseParamCommentByExampleUnsupportedTypeV3(t *testing.T) { + t.Parallel() + var param spec.Parameter + + setExampleV3(¶m, "something", "random value") + assert.Equal(t, param.Example, nil) + + setExampleV3(¶m, STRING, "string value") + assert.Equal(t, param.Example, "string value") + + setExampleV3(¶m, INTEGER, "10") + assert.Equal(t, param.Example, 10) + + setExampleV3(¶m, NUMBER, "10") + assert.Equal(t, param.Example, float64(10)) +} + +func TestParseParamCommentBySchemaExampleStringV3(t *testing.T) { + t.Parallel() + + comment := `@Param some_id body string true "Some ID" SchemaExample(True feelings)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, "True feelings", parameterSpec.Schema.Spec.Example) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) +} + +func TestParseParamCommentBySchemaExampleUnsupportedTypeV3(t *testing.T) { + t.Parallel() + var param spec.Parameter + + setSchemaExampleV3(¶m, "something", "random value") + assert.Nil(t, param.Schema) + + setSchemaExampleV3(¶m, STRING, "string value") + assert.Nil(t, param.Schema) + + param.Schema = spec.NewSchemaSpec() + setSchemaExampleV3(¶m, STRING, "string value") + assert.Equal(t, "string value", param.Schema.Spec.Example) + + setSchemaExampleV3(¶m, INTEGER, "10") + assert.Equal(t, 10, param.Schema.Spec.Example) + + setSchemaExampleV3(¶m, NUMBER, "10") + assert.Equal(t, float64(10), param.Schema.Spec.Example) + + setSchemaExampleV3(¶m, STRING, "string \\r\\nvalue") + assert.Equal(t, "string \r\nvalue", param.Schema.Spec.Example) +} + +func TestParseParamArrayWithEnumsV3(t *testing.T) { + t.Parallel() + + comment := `@Param field query []string true "An enum collection" collectionFormat(csv) enums(also,valid)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "An enum collection", parameterSpec.Description) + assert.Equal(t, "field", parameterSpec.Name) + assert.True(t, parameterSpec.Required) + assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.Equal(t, "form", parameterSpec.Style) + + enums := []interface{}{"also", "valid"} + assert.EqualValues(t, enums, parameterSpec.Schema.Spec.Items.Schema.Spec.Enum) + assert.Equal(t, typeString, parameterSpec.Schema.Spec.Items.Schema.Spec.Type) +} + +func TestParseAndExtractionParamAttributeV3(t *testing.T) { + t.Parallel() + + op := NewOperationV3(New()) + + t.Run("number", func(t *testing.T) { + numberParam := spec.Parameter{ + Schema: spec.NewSchemaSpec(), + } + err := op.parseParamAttribute( + " default(1) maximum(100) minimum(0) format(csv)", + "", + NUMBER, + &numberParam, + ) + assert.NoError(t, err) + assert.Equal(t, int(0), *numberParam.Schema.Spec.Minimum) + assert.Equal(t, int(100), *numberParam.Schema.Spec.Maximum) + assert.Equal(t, "csv", numberParam.Schema.Spec.Format) + assert.Equal(t, float64(1), numberParam.Schema.Spec.Default) + + err = op.parseParamAttribute(" minlength(1)", "", NUMBER, nil) + assert.Error(t, err) + + err = op.parseParamAttribute(" maxlength(1)", "", NUMBER, nil) + assert.Error(t, err) + }) + + t.Run("string", func(t *testing.T) { + stringParam := spec.Parameter{ + Schema: spec.NewSchemaSpec(), + } + err := op.parseParamAttribute( + " default(test) maxlength(100) minlength(0) format(csv)", + "", + STRING, + &stringParam, + ) + assert.NoError(t, err) + assert.Equal(t, int(0), *stringParam.Schema.Spec.MinLength) + assert.Equal(t, int(100), *stringParam.Schema.Spec.MaxLength) + assert.Equal(t, "csv", stringParam.Schema.Spec.Format) + err = op.parseParamAttribute(" minimum(0)", "", STRING, nil) + assert.Error(t, err) + + err = op.parseParamAttribute(" maximum(0)", "", STRING, nil) + assert.Error(t, err) + }) + + t.Run("array", func(t *testing.T) { + arrayParam := spec.Parameter{ + Schema: spec.NewSchemaSpec(), + } + + arrayParam.In = "path" + err := op.parseParamAttribute(" collectionFormat(simple)", ARRAY, STRING, &arrayParam) + assert.Equal(t, "simple", arrayParam.Style) + assert.NoError(t, err) + + err = op.parseParamAttribute(" collectionFormat(simple)", STRING, STRING, nil) + assert.Error(t, err) + + err = op.parseParamAttribute(" default(0)", "", ARRAY, nil) + assert.NoError(t, err) + }) +} + +func TestParseParamCommentByExtensionsV3(t *testing.T) { + comment := `@Param some_id path int true "Some ID" extensions(x-example=test,x-custom=Gopher,x-custom2)` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "Some ID", parameterSpec.Description) + assert.Equal(t, "some_id", parameterSpec.Name) + assert.Equal(t, "path", parameterSpec.In) + assert.True(t, parameterSpec.Required) + assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + assert.Equal(t, "Gopher", parameterSpec.Schema.Spec.Extensions["x-custom"]) + assert.Equal(t, true, parameterSpec.Schema.Spec.Extensions["x-custom2"]) + assert.Equal(t, "test", parameterSpec.Schema.Spec.Extensions["x-example"]) +} + +func TestParseIdCommentV3(t *testing.T) { + t.Parallel() + + comment := `@Id myOperationId` + operation := NewOperationV3(nil) + err := operation.ParseComment(comment, nil) + + assert.NoError(t, err) + assert.Equal(t, "myOperationId", operation.Operation.OperationID) +} + +func TestParseSecurityCommentV3(t *testing.T) { + t.Parallel() + + comment := `@Security OAuth2Implicit[read, write]` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + expected := []spec.SecurityRequirement{{ + "OAuth2Implicit": {"read", "write"}, + }} + + assert.Equal(t, expected, operation.Security) +} + +func TestParseSecurityCommentSimpleV3(t *testing.T) { + t.Parallel() + + comment := `@Security ApiKeyAuth` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + expected := []spec.SecurityRequirement{{ + "ApiKeyAuth": {}, + }} + + assert.Equal(t, expected, operation.Security) +} + +func TestParseSecurityCommentOrV3(t *testing.T) { + t.Parallel() + + comment := `@Security OAuth2Implicit[read, write] || Firebase[]` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + expected := []spec.SecurityRequirement{{ + "OAuth2Implicit": {"read", "write"}, + "Firebase": {""}, + }} + + assert.Equal(t, expected, operation.Security) +} + +func TestParseMultiDescriptionV3(t *testing.T) { + t.Parallel() + + comment := `@Description line one` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + comment = `@Tags multi` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err) + + comment = `@Description line two x` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Equal(t, "line one\nline two x", operation.Description) +} + +func TestParseDescriptionMarkdownV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(New()) + operation.parser.markdownFileDir = "example/markdown" + + comment := `@description.markdown admin.md` + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + comment = `@description.markdown missing.md` + + err = operation.ParseComment(comment, nil) + assert.Error(t, err) +} + +func TestParseSummaryV3(t *testing.T) { + t.Parallel() + + comment := `@summary line one` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + assert.Equal(t, "line one", operation.Summary) + + comment = `@Summary line one` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err) +} + +func TestParseDeprecationDescriptionV3(t *testing.T) { + t.Parallel() + + comment := `@Deprecated` + operation := NewOperationV3(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + assert.True(t, operation.Deprecated) +} + +func TestParseExtensionsV3(t *testing.T) { + t.Parallel() + // Fail if there are no args for attributes. + { + comment := `@x-amazon-apigateway-integration` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a value") + } + + // Fail if args of attributes are broken. + { + comment := `@x-amazon-apigateway-integration ["broken"}]` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a valid json value. error: invalid character '}' after array element") + } + + // OK + { + comment := `@x-amazon-apigateway-integration {"uri": "${some_arn}", "passthroughBehavior": "when_no_match", "httpMethod": "POST", "type": "aws_proxy"}` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + assert.Equal(t, map[string]interface{}{ + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": "${some_arn}", + }, operation.Responses.Extensions["x-amazon-apigateway-integration"]) + } + + // Test x-tagGroups + { + comment := `@x-tagGroups [{"name":"Natural Persons","tags":["Person","PersonRisk","PersonDocuments"]}]` + operation := NewOperationV3(New()) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + assert.Equal(t, + []interface{}{map[string]interface{}{ + "name": "Natural Persons", + "tags": []interface{}{ + "Person", + "PersonRisk", + "PersonDocuments", + }, + }}, operation.Responses.Extensions["x-tagGroups"]) + } +} + +func TestParseResponseHeaderCommentV3(t *testing.T) { + t.Parallel() + + operation := NewOperationV3(New()) + + err := operation.ParseResponseComment(`default {string} string "other error"`, nil) + assert.NoError(t, err) + err = operation.ParseResponseHeaderComment(`all {string} Token "qwerty"`, nil) + assert.NoError(t, err) +} + +func TestParseCodeSamplesV3(t *testing.T) { + t.Parallel() + const comment = `@x-codeSamples file` + t.Run("Find sample by file", func(t *testing.T) { + + operation := NewOperationV3(New(), SetCodeExampleFilesDirectoryV3("testdata/code_examples")) + operation.Summary = "example" + + err := operation.ParseComment(comment, nil) + require.NoError(t, err, "no error should be thrown") + + assert.Equal(t, "example", operation.Summary) + assert.Equal(t, CodeSamples(CodeSamples{map[string]string{"lang": "JavaScript", "source": "console.log('Hello World');"}}), + operation.Responses.Extensions["x-codeSamples"], + ) + }) + + t.Run("With broken file sample", func(t *testing.T) { + operation := NewOperationV3(New(), SetCodeExampleFilesDirectoryV3("testdata/code_examples")) + operation.Summary = "broken" + + err := operation.ParseComment(comment, nil) + assert.Error(t, err, "no error should be thrown") + }) + + t.Run("Example file not found", func(t *testing.T) { + operation := NewOperationV3(New(), SetCodeExampleFilesDirectoryV3("testdata/code_examples")) + operation.Summary = "badExample" + + err := operation.ParseComment(comment, nil) + assert.Error(t, err, "error was expected, as file does not exist") + }) + + t.Run("Without line reminder", func(t *testing.T) { + comment := `@x-codeSamples` + operation := NewOperationV3(New(), SetCodeExampleFilesDirectoryV3("testdata/code_examples")) + operation.Summary = "example" + + err := operation.ParseComment(comment, nil) + assert.Error(t, err, "no error should be thrown") + }) + + t.Run(" broken dir", func(t *testing.T) { + operation := NewOperationV3(New(), SetCodeExampleFilesDirectoryV3("testdata/fake_examples")) + operation.Summary = "code" + + err := operation.ParseComment(comment, nil) + assert.Error(t, err, "no error should be thrown") + }) +} diff --git a/packages.go b/packages.go index 2c5693fc4..4bf31b751 100644 --- a/packages.go +++ b/packages.go @@ -133,6 +133,7 @@ func (pkgDefs *PackagesDefinitions) parseTypesFromFile(astFile *ast.File, packag if !ok { continue } + if generalDeclaration.Tok == token.TYPE { for _, astSpec := range generalDeclaration.Specs { if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { @@ -245,6 +246,7 @@ func (pkgDefs *PackagesDefinitions) parseFunctionScopedTypesFromFile(astFile *as } } + } } } @@ -548,7 +550,18 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File } pkgPaths, externalPkgPaths := pkgDefs.findPackagePathFromImports(parts[0], file) - typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, parts[1]) + + if len(pkgPaths) == 0 && len(externalPkgPaths) == 0 { + pkgDefinition := pkgDefs.packages["pkg/"+parts[0]] + if pkgDefinition == nil { + return pkgDefs.findTypeSpec("", parts[1]) + } + + typeDef = pkgDefinition.TypeDefinitions[parts[1]] + } else { + typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, parts[1]) + } + return pkgDefs.parametrizeGenericType(file, typeDef, typeName) } @@ -567,7 +580,32 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File typeDef, ok = pkgDefs.uniqueDefinitions[fullTypeName(file.Name.Name, name)] if !ok { pkgPaths, externalPkgPaths := pkgDefs.findPackagePathFromImports("", file) - typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, name) + + if len(pkgPaths) == 0 { + pkgDefinition := pkgDefs.packages["pkg/"+parts[0]] + if pkgDefinition == nil { + return pkgDefs.findTypeSpec("", parts[1]) + } + + typeDef = pkgDefinition.TypeDefinitions[parts[0]] + } else { + typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, name) + } } + return pkgDefs.parametrizeGenericType(file, typeDef, typeName) } + +func isAliasPkgName(file *ast.File, pkgName string) bool { + if file == nil && file.Imports == nil { + return false + } + + for _, pkg := range file.Imports { + if pkg.Name != nil && pkg.Name.Name == pkgName { + return true + } + } + + return false +} diff --git a/packages_test.go b/packages_test.go index 595659ba1..b1734fc12 100644 --- a/packages_test.go +++ b/packages_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestPackagesDefinitions_ParseFile(t *testing.T) { +func Test_PackagesDefinitions_ParseFile(t *testing.T) { pd := PackagesDefinitions{} packageDir := "github.com/swaggo/swag/testdata/simple" assert.NoError(t, pd.ParseFile(packageDir, "testdata/simple/main.go", nil, ParseAll)) diff --git a/parser.go b/parser.go index 2e453960a..916a8620c 100644 --- a/parser.go +++ b/parser.go @@ -3,7 +3,6 @@ package swag import ( "context" "encoding/json" - "errors" "fmt" "go/ast" "go/build" @@ -19,8 +18,11 @@ import ( "strconv" "strings" + "github.com/pkg/errors" + "github.com/KyleBanks/depth" "github.com/go-openapi/spec" + openapi "github.com/sv-tools/openapi/spec" ) const ( @@ -33,39 +35,40 @@ const ( // SnakeCase indicates using SnakeCase strategy for struct field. SnakeCase = "snakecase" - idAttr = "@id" - acceptAttr = "@accept" - produceAttr = "@produce" - paramAttr = "@param" - successAttr = "@success" - failureAttr = "@failure" - responseAttr = "@response" - headerAttr = "@header" - tagsAttr = "@tags" - routerAttr = "@router" - summaryAttr = "@summary" - deprecatedAttr = "@deprecated" - securityAttr = "@security" - titleAttr = "@title" - conNameAttr = "@contact.name" - conURLAttr = "@contact.url" - conEmailAttr = "@contact.email" - licNameAttr = "@license.name" - licURLAttr = "@license.url" - versionAttr = "@version" - descriptionAttr = "@description" - descriptionMarkdownAttr = "@description.markdown" - secBasicAttr = "@securitydefinitions.basic" - secAPIKeyAttr = "@securitydefinitions.apikey" - secApplicationAttr = "@securitydefinitions.oauth2.application" - secImplicitAttr = "@securitydefinitions.oauth2.implicit" - secPasswordAttr = "@securitydefinitions.oauth2.password" - secAccessCodeAttr = "@securitydefinitions.oauth2.accesscode" - tosAttr = "@termsofservice" - extDocsDescAttr = "@externaldocs.description" - extDocsURLAttr = "@externaldocs.url" - xCodeSamplesAttr = "@x-codesamples" - scopeAttrPrefix = "@scope." + idAttr = "@id" + acceptAttr = "@accept" + produceAttr = "@produce" + paramAttr = "@param" + successAttr = "@success" + failureAttr = "@failure" + responseAttr = "@response" + headerAttr = "@header" + tagsAttr = "@tags" + routerAttr = "@router" + summaryAttr = "@summary" + deprecatedAttr = "@deprecated" + securityAttr = "@security" + titleAttr = "@title" + conNameAttr = "@contact.name" + conURLAttr = "@contact.url" + conEmailAttr = "@contact.email" + licNameAttr = "@license.name" + licURLAttr = "@license.url" + versionAttr = "@version" + descriptionAttr = "@description" + descriptionMarkdownAttr = "@description.markdown" + secBasicAttr = "@securitydefinitions.basic" + secAPIKeyAttr = "@securitydefinitions.apikey" + secApplicationAttr = "@securitydefinitions.oauth2.application" + secImplicitAttr = "@securitydefinitions.oauth2.implicit" + secPasswordAttr = "@securitydefinitions.oauth2.password" + secAccessCodeAttr = "@securitydefinitions.oauth2.accesscode" + tosAttr = "@termsofservice" + extDocsDescAttr = "@externaldocs.description" + extDocsURLAttr = "@externaldocs.url" + xCodeSamplesAttr = "@x-codesamples" + xCodeSamplesAttrOriginal = "@x-codeSamples" + scopeAttrPrefix = "@scope." ) // ParseFlag determine what to parse @@ -111,15 +114,24 @@ type Parser struct { // swagger represents the root document object for the API specification swagger *spec.Swagger + // openAPI represents the v3.1 root document object for the API specification + openAPI *openapi.OpenAPI + // packages store entities of APIs, definitions, file, package path etc. and their relations packages *PackagesDefinitions // parsedSchemas store schemas which have been parsed from ast.TypeSpec parsedSchemas map[*TypeSpecDef]*Schema + // parsedSchemasV3 store schemas which have been parsed from ast.TypeSpec + parsedSchemasV3 map[*TypeSpecDef]*SchemaV3 + // outputSchemas store schemas which will be export to swagger outputSchemas map[*TypeSpecDef]*Schema + // outputSchemas store schemas which will be export to swagger + outputSchemasV3 map[*TypeSpecDef]*SchemaV3 + // PropNamingStrategy naming strategy PropNamingStrategy string @@ -162,6 +174,9 @@ type Parser struct { // fieldParserFactory create FieldParser fieldParserFactory FieldParserFactory + // fieldParserFactoryV3 create FieldParser + fieldParserFactoryV3 FieldParserFactoryV3 + // Overrides allows global replacements of types. A blank replacement will be skipped. Overrides map[string]string @@ -170,6 +185,9 @@ type Parser struct { // tags to filter the APIs after tags map[string]struct{} + + // use new openAPI version + openAPIVersion bool } // FieldParserFactory create FieldParser. @@ -214,17 +232,31 @@ func New(options ...func(*Parser)) *Parser { SecurityDefinitions: make(map[string]*spec.SecurityScheme), }, VendorExtensible: spec.VendorExtensible{ - Extensions: nil, + Extensions: make(spec.Extensions), }, }, - packages: NewPackagesDefinitions(), - debug: log.New(os.Stdout, "", log.LstdFlags), - parsedSchemas: make(map[*TypeSpecDef]*Schema), - outputSchemas: make(map[*TypeSpecDef]*Schema), - excludes: make(map[string]struct{}), - tags: make(map[string]struct{}), - fieldParserFactory: newTagBaseFieldParser, - Overrides: make(map[string]string), + openAPI: &openapi.OpenAPI{ + Info: openapi.NewInfo(), + OpenAPI: "3.1.0", + Components: openapi.NewComponents(), + ExternalDocs: openapi.NewExternalDocs(), + Paths: openapi.NewPaths(), + WebHooks: map[string]*openapi.RefOrSpec[openapi.Extendable[openapi.PathItem]]{}, + Security: []openapi.SecurityRequirement{}, + Tags: []*openapi.Extendable[openapi.Tag]{}, + Servers: []*openapi.Extendable[openapi.Server]{}, + }, + packages: NewPackagesDefinitions(), + debug: log.New(os.Stdout, "", log.LstdFlags), + parsedSchemas: make(map[*TypeSpecDef]*Schema), + parsedSchemasV3: make(map[*TypeSpecDef]*SchemaV3), + outputSchemas: make(map[*TypeSpecDef]*Schema), + outputSchemasV3: make(map[*TypeSpecDef]*SchemaV3), + excludes: make(map[string]struct{}), + tags: make(map[string]struct{}), + fieldParserFactory: newTagBaseFieldParser, + fieldParserFactoryV3: newTagBaseFieldParserV3, + Overrides: make(map[string]string), } for _, option := range options { @@ -338,6 +370,13 @@ func ParseUsingGoList(enabled bool) func(parser *Parser) { } } +// SetOpenAPIVersion parses only those operations which match given extension +func SetOpenAPIVersion(openAPIVersion bool) func(*Parser) { + return func(p *Parser) { + p.openAPIVersion = openAPIVersion + } +} + // ParseAPI parses general api info for given searchDir and mainAPIFile. func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string, parseDepth int) error { return parser.ParseAPIMultiSearchDir([]string{searchDir}, mainAPIFile, parseDepth) @@ -366,40 +405,7 @@ func (parser *Parser) ParseAPIMultiSearchDir(searchDirs []string, mainAPIFile st // Use 'go list' command instead of depth.Resolve() if parser.ParseDependency { - if parser.parseGoList { - pkgs, err := listPackages(context.Background(), filepath.Dir(absMainAPIFilePath), nil, "-deps") - if err != nil { - return fmt.Errorf("pkg %s cannot find all dependencies, %s", filepath.Dir(absMainAPIFilePath), err) - } - - length := len(pkgs) - for i := 0; i < length; i++ { - err := parser.getAllGoFileInfoFromDepsByList(pkgs[i]) - if err != nil { - return err - } - } - } else { - var t depth.Tree - t.ResolveInternal = true - t.MaxDepth = parseDepth - - pkgName, err := getPkgName(filepath.Dir(absMainAPIFilePath)) - if err != nil { - return err - } - - err = t.Resolve(pkgName) - if err != nil { - return fmt.Errorf("pkg %s cannot find all dependencies, %s", pkgName, err) - } - for i := 0; i < len(t.Root.Deps); i++ { - err := parser.getAllGoFileInfoFromDeps(&t.Root.Deps[i]) - if err != nil { - return err - } - } - } + parser.parseDeps(absMainAPIFilePath, parseDepth) } err = parser.ParseGeneralAPIInfo(absMainAPIFilePath) @@ -412,14 +418,59 @@ func (parser *Parser) ParseAPIMultiSearchDir(searchDirs []string, mainAPIFile st return err } - err = parser.packages.RangeFiles(parser.ParseRouterAPIInfo) - if err != nil { - return err + if parser.openAPIVersion { + err = parser.packages.RangeFiles(parser.ParseRouterAPIInfoV3) + if err != nil { + return err + } + } else { + err = parser.packages.RangeFiles(parser.ParseRouterAPIInfo) + if err != nil { + return err + } } return parser.checkOperationIDUniqueness() } +func (parser *Parser) parseDeps(absMainAPIFilePath string, parseDepth int) error { + if parser.parseGoList { + pkgs, err := listPackages(context.Background(), filepath.Dir(absMainAPIFilePath), nil, "-deps") + if err != nil { + return fmt.Errorf("pkg %s cannot find all dependencies, %s", filepath.Dir(absMainAPIFilePath), err) + } + + length := len(pkgs) + for i := 0; i < length; i++ { + err := parser.getAllGoFileInfoFromDepsByList(pkgs[i]) + if err != nil { + return err + } + } + } else { + var t depth.Tree + t.ResolveInternal = true + t.MaxDepth = parseDepth + + pkgName, err := getPkgName(absMainAPIFilePath) + if err != nil { + return errors.Wrap(err, "could not parse dependencies") + } + + if err := t.Resolve(pkgName); err != nil { + return errors.Wrap(fmt.Errorf("pkg %s cannot find all dependencies, %s", pkgName, err), "could not resolve dependencies") + } + + for i := 0; i < len(t.Root.Deps); i++ { + if err := parser.getAllGoFileInfoFromDeps(&t.Root.Deps[i]); err != nil { + return errors.Wrap(err, "could not parse dependencies") + } + } + } + + return nil +} + func getPkgName(searchDir string) (string, error) { cmd := exec.Command("go", "list", "-f={{.ImportPath}}") cmd.Dir = searchDir @@ -429,6 +480,8 @@ func getPkgName(searchDir string) (string, error) { cmd.Stdout = &stdout cmd.Stderr = &stderr + fmt.Println("get pkg name for directory:", searchDir) + if err := cmd.Run(); err != nil { return "", fmt.Errorf("execute go list command, %s, stdout:%s, stderr:%s", err, stdout.String(), stderr.String()) } @@ -448,16 +501,30 @@ func getPkgName(searchDir string) (string, error) { // ParseGeneralAPIInfo parses general api info for given mainAPIFile path. func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error { - fileTree, err := goparser.ParseFile(token.NewFileSet(), mainAPIFile, nil, goparser.ParseComments) + fileSet := token.NewFileSet() + filePath := mainAPIFile + + fileTree, err := goparser.ParseFile(fileSet, filePath, nil, goparser.ParseComments) if err != nil { - return fmt.Errorf("cannot parse source files %s: %s", mainAPIFile, err) + return fmt.Errorf("cannot parse source files %s: %s", filePath, err) } parser.swagger.Swagger = "2.0" - for _, comment := range fileTree.Comments { + for i := range fileTree.Comments { + comment := fileTree.Comments[i] + if !isGeneralAPIComment(comment.Text()) { + continue + } + comments := strings.Split(comment.Text(), "\n") - if !isGeneralAPIComment(comments) { + + if parser.openAPIVersion { + err = parser.parseGeneralAPIInfoV3(comments) + if err != nil { + return err + } + continue } @@ -465,6 +532,7 @@ func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error { if err != nil { return err } + } return nil @@ -583,6 +651,18 @@ func parseGeneralAPIInfo(parser *Parser, comments []string) error { parser.swagger.ExternalDocs.URL = value } + case "@x-taggroups": + originalAttribute := strings.Split(commentLine, " ")[0] + if len(value) == 0 { + return fmt.Errorf("annotation %s need a value", attribute) + } + + var valueJSON interface{} + if err := json.Unmarshal([]byte(value), &valueJSON); err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", originalAttribute, err.Error()) + } + + parser.swagger.Extensions[originalAttribute[1:]] = valueJSON // don't use the method provided by spec lib, cause it will call toLower() on attribute names, which is wrongy default: if strings.HasPrefix(attribute, "@x-") { extensionName := attribute[1:] @@ -610,7 +690,7 @@ func parseGeneralAPIInfo(parser *Parser, comments []string) error { var valueJSON interface{} err := json.Unmarshal([]byte(value), &valueJSON) if err != nil { - return fmt.Errorf("annotation %s need a valid json value", attribute) + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) } if strings.Contains(extensionName, "logo") { @@ -695,13 +775,14 @@ func parseSecAttributes(context string, lines []string, index *int) (*spec.Secur fields := FieldsByAnySpace(v, 2) securityAttr := strings.ToLower(fields[0]) + var value string if len(fields) > 1 { value = fields[1] } - for _, findterm := range search { - if securityAttr == findterm { + for _, findTerm := range search { + if securityAttr == findTerm { attrMap[securityAttr] = value break @@ -786,19 +867,20 @@ func (parser *Parser) ParseProduceComment(commentLine string) error { return parseMimeTypeList(commentLine, &parser.swagger.Produces, "%v produce type can't be accepted") } -func isGeneralAPIComment(comments []string) bool { - for _, commentLine := range comments { - commentLine = strings.TrimSpace(commentLine) - if len(commentLine) == 0 { - continue - } - attribute := strings.ToLower(FieldsByAnySpace(commentLine, 2)[0]) - switch attribute { - // The @summary, @router, @success, @failure annotation belongs to Operation - case summaryAttr, routerAttr, successAttr, failureAttr, responseAttr: - return false - } +func isGeneralAPIComment(comment string) bool { + // for _, commentLine := range comments { + commentLine := strings.TrimSpace(comment) + if len(commentLine) == 0 { + return false + } + + attribute := strings.ToLower(FieldsByAnySpace(commentLine, 2)[0]) + switch attribute { + // The @summary, @router, @success, @failure annotation belongs to Operation + case summaryAttr, routerAttr, successAttr, failureAttr, responseAttr: + return false } + // } return true } @@ -873,6 +955,7 @@ func (parser *Parser) matchTags(comments []*ast.Comment) (match bool) { if _, has := parser.tags["!"+tag]; has { return false } + if _, has := parser.tags[tag]; has { match = true // keep iterating as it may contain a tag that is excluded } @@ -880,25 +963,28 @@ func (parser *Parser) matchTags(comments []*ast.Comment) (match bool) { } return } + return true } func matchExtension(extensionToMatch string, comments []*ast.Comment) (match bool) { - if len(extensionToMatch) != 0 { - for _, comment := range comments { - commentLine := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) - fields := FieldsByAnySpace(commentLine, 2) - if len(fields) > 0 { - lowerAttribute := strings.ToLower(fields[0]) + if len(extensionToMatch) == 0 { + return true + } - if lowerAttribute == fmt.Sprintf("@x-%s", strings.ToLower(extensionToMatch)) { - return true - } + for _, comment := range comments { + commentLine := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) + fields := FieldsByAnySpace(commentLine, 2) + if len(fields) > 0 { + lowerAttribute := strings.ToLower(fields[0]) + + if lowerAttribute == fmt.Sprintf("@x-%s", strings.ToLower(extensionToMatch)) { + return true } } - return false } - return true + + return false } // ParseRouterAPIInfo parses router api info for given astFile. @@ -907,12 +993,14 @@ func (parser *Parser) ParseRouterAPIInfo(fileInfo *AstFileInfo) error { if (fileInfo.ParseFlag & ParseOperations) == ParseNone { continue } + 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 { @@ -923,6 +1011,7 @@ func (parser *Parser) ParseRouterAPIInfo(fileInfo *AstFileInfo) error { if err != nil { return err } + } } } @@ -1019,6 +1108,7 @@ func (parser *Parser) getTypeSchema(typeName string, file *ast.File, ref bool) ( typeSpecDef := parser.packages.FindTypeSpec(typeName, file) if typeSpecDef == nil { + parser.packages.FindTypeSpec(typeName, file) // uncomment for debugging return nil, fmt.Errorf("cannot find type definition: %s", typeName) } @@ -1560,6 +1650,8 @@ func defineTypeOfExample(schemaType, arrayType, exampleValue string) (interface{ } return result, nil + case ANY: + return exampleValue, nil } return nil, fmt.Errorf("%s is unsupported type in example value %s", schemaType, exampleValue) @@ -1706,4 +1798,10 @@ func (parser *Parser) addTestType(typename string) { Name: typename, Schema: PrimitiveSchema(OBJECT), } + + parser.parsedSchemasV3[typeDef] = &SchemaV3{ + PkgPath: "", + Name: typename, + Schema: PrimitiveSchemaV3(OBJECT).Spec, + } } diff --git a/parser_test.go b/parser_test.go index f070376db..ea29728bb 100644 --- a/parser_test.go +++ b/parser_test.go @@ -371,7 +371,7 @@ func TestParser_ParseGeneralApiInfoExtensions(t *testing.T) { t.Run("Test invalid extension value", func(t *testing.T) { t.Parallel() - expected := "annotation @x-google-endpoints need a valid json value" + expected := "annotation @x-google-endpoints need a valid json value. error: invalid character ':' after array element" gopath := os.Getenv("GOPATH") assert.NotNil(t, gopath) diff --git a/parserv3.go b/parserv3.go new file mode 100644 index 000000000..b0f49c5df --- /dev/null +++ b/parserv3.go @@ -0,0 +1,995 @@ +package swag + +import ( + "encoding/json" + "fmt" + "go/ast" + "go/token" + "net/http" + "reflect" + "sort" + "strings" + + "github.com/pkg/errors" + "github.com/sv-tools/openapi/spec" +) + +// FieldParserFactory create FieldParser. +type FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 + +// FieldParser parse struct field. +type FieldParserV3 interface { + ShouldSkip() bool + FieldName() (string, error) + FormName() string + CustomSchema() (*spec.RefOrSpec[spec.Schema], error) + ComplementSchema(schema *spec.RefOrSpec[spec.Schema]) error + IsRequired() (bool, error) +} + +// GetOpenAPI returns *spec.OpenAPI which is the root document object for the API specification. +func (parser *Parser) GetOpenAPI() *spec.OpenAPI { + return parser.openAPI +} + +func (p *Parser) parseGeneralAPIInfoV3(comments []string) error { + previousAttribute := "" + + // parsing classic meta data model + for line := 0; line < len(comments); line++ { + commentLine := comments[line] + commentLine = strings.TrimSpace(commentLine) + if len(commentLine) == 0 { + continue + } + fields := FieldsByAnySpace(commentLine, 2) + + attribute := fields[0] + var value string + if len(fields) > 1 { + value = fields[1] + } + + switch attr := strings.ToLower(attribute); attr { + case versionAttr, titleAttr, tosAttr, licNameAttr, licURLAttr, conNameAttr, conURLAttr, conEmailAttr: + setspecInfo(p.openAPI, attr, value) + case descriptionAttr: + if previousAttribute == attribute { + p.openAPI.Info.Spec.Description += "\n" + value + + continue + } + + setspecInfo(p.openAPI, attr, value) + case descriptionMarkdownAttr: + commentInfo, err := getMarkdownForTag("api", p.markdownFileDir) + if err != nil { + return err + } + + setspecInfo(p.openAPI, attr, string(commentInfo)) + case "@host": + if len(p.openAPI.Servers) == 0 { + server := spec.NewServer() + server.Spec.URL = value + p.openAPI.Servers = append(p.openAPI.Servers, server) + } + + println("@host is deprecated use servers instead") + case "@basepath": + if len(p.openAPI.Servers) == 0 { + server := spec.NewServer() + p.openAPI.Servers = append(p.openAPI.Servers, server) + } + p.openAPI.Servers[0].Spec.URL += value + + println("@basepath is deprecated use servers instead") + + case acceptAttr: + println("acceptAttribute is deprecated, as there is no such field on top level in spec V3.1") + case produceAttr: + println("produce is deprecated, as there is no such field on top level in spec V3.1") + case "@schemes": + println("@schemes is deprecated use servers instead") + case "@tag.name": + tag := &spec.Extendable[spec.Tag]{ + Spec: &spec.Tag{ + Name: value, + }, + } + + p.openAPI.Tags = append(p.openAPI.Tags, tag) + case "@tag.description": + tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] + tag.Spec.Description = value + case "@tag.description.markdown": + tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] + + commentInfo, err := getMarkdownForTag(tag.Spec.Name, p.markdownFileDir) + if err != nil { + return err + } + + tag.Spec.Description = string(commentInfo) + case "@tag.docs.url": + tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] + tag.Spec.ExternalDocs = spec.NewExternalDocs() + tag.Spec.ExternalDocs.Spec.URL = value + case "@tag.docs.description": + tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] + if tag.Spec.ExternalDocs == nil { + return fmt.Errorf("%s needs to come after a @tags.docs.url", attribute) + } + + tag.Spec.ExternalDocs.Spec.Description = value + case secBasicAttr, secAPIKeyAttr, secApplicationAttr, secImplicitAttr, secPasswordAttr, secAccessCodeAttr: + key, scheme, err := parseSecAttributesV3(attribute, comments, &line) + if err != nil { + return err + } + + schemeSpec := spec.NewSecuritySchemeSpec() + schemeSpec.Spec.Spec = scheme + + if p.openAPI.Components.Spec.SecuritySchemes == nil { + p.openAPI.Components.Spec.SecuritySchemes = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.SecurityScheme]]) + } + + p.openAPI.Components.Spec.SecuritySchemes[key] = schemeSpec + + case "@query.collection.format": + p.collectionFormatInQuery = TransToValidCollectionFormat(value) + + case extDocsDescAttr, extDocsURLAttr: + if p.openAPI.ExternalDocs == nil { + p.openAPI.ExternalDocs = spec.NewExternalDocs() + } + + switch attr { + case extDocsDescAttr: + p.openAPI.ExternalDocs.Spec.Description = value + case extDocsURLAttr: + p.openAPI.ExternalDocs.Spec.URL = value + } + + case "@x-taggroups": + originalAttribute := strings.Split(commentLine, " ")[0] + if len(value) == 0 { + return fmt.Errorf("annotation %s need a value", attribute) + } + + var valueJSON interface{} + if err := json.Unmarshal([]byte(value), &valueJSON); err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", originalAttribute, err.Error()) + } + + p.openAPI.Info.Extensions[originalAttribute[1:]] = valueJSON + default: + if strings.HasPrefix(attribute, "@x-") { + err := p.parseExtensionsV3(value, attribute) + if err != nil { + return errors.Wrap(err, "could not parse extension comment") + } + } + } + + previousAttribute = attribute + } + + return nil +} + +func (p *Parser) parseExtensionsV3(value, attribute string) error { + extensionName := attribute[1:] + + // // for each security definition + // for _, v := range p.openAPI.Components.Spec.SecuritySchemes{ + // // check if extension exists + // _, extExistsInSecurityDef := v.VendorExtensible.Extensions.GetString(extensionName) + // // if it exists in at least one, then we stop iterating + // if extExistsInSecurityDef { + // return nil + // } + // } + + if len(value) == 0 { + return fmt.Errorf("annotation %s need a value", attribute) + } + + if p.openAPI.Info.Extensions == nil { + p.openAPI.Info.Extensions = map[string]any{} + } + + var valueJSON interface{} + err := json.Unmarshal([]byte(value), &valueJSON) + if err != nil { + return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) + } + + if strings.Contains(extensionName, "logo") { + p.openAPI.Info.Extensions[extensionName] = valueJSON + return nil + } + + p.openAPI.Info.Extensions[attribute[1:]] = valueJSON + + return nil +} + +func setspecInfo(openAPI *spec.OpenAPI, attribute, value string) { + switch attribute { + case versionAttr: + openAPI.Info.Spec.Version = value + case titleAttr: + openAPI.Info.Spec.Title = value + case tosAttr: + openAPI.Info.Spec.TermsOfService = value + case descriptionAttr: + openAPI.Info.Spec.Description = value + case conNameAttr: + if openAPI.Info.Spec.Contact == nil { + openAPI.Info.Spec.Contact = spec.NewContact() + } + + openAPI.Info.Spec.Contact.Spec.Name = value + case conEmailAttr: + if openAPI.Info.Spec.Contact == nil { + openAPI.Info.Spec.Contact = spec.NewContact() + } + + openAPI.Info.Spec.Contact.Spec.Email = value + case conURLAttr: + if openAPI.Info.Spec.Contact == nil { + openAPI.Info.Spec.Contact = spec.NewContact() + } + + openAPI.Info.Spec.Contact.Spec.URL = value + case licNameAttr: + if openAPI.Info.Spec.License == nil { + openAPI.Info.Spec.License = spec.NewLicense() + } + openAPI.Info.Spec.License.Spec.Name = value + case licURLAttr: + if openAPI.Info.Spec.License == nil { + openAPI.Info.Spec.License = spec.NewLicense() + } + openAPI.Info.Spec.License.Spec.URL = value + } +} + +func parseSecAttributesV3(context string, lines []string, index *int) (string, *spec.SecurityScheme, error) { + const ( + in = "@in" + name = "@name" + descriptionAttr = "@description" + tokenURL = "@tokenurl" + authorizationURL = "@authorizationurl" + ) + + var search []string + + attribute := strings.ToLower(FieldsByAnySpace(lines[*index], 2)[0]) + switch attribute { + case secBasicAttr: + scheme := spec.SecurityScheme{ + Type: "http", + Scheme: "basic", + } + return "basic", &scheme, nil + case secAPIKeyAttr: + search = []string{in, name} + case secApplicationAttr, secPasswordAttr: + search = []string{tokenURL, in, name} + case secImplicitAttr: + search = []string{authorizationURL, in} + case secAccessCodeAttr: + search = []string{tokenURL, authorizationURL, in} + } + + // For the first line we get the attributes in the context parameter, so we skip to the next one + *index++ + + attrMap, scopes := make(map[string]string), make(map[string]string) + extensions, description := make(map[string]interface{}), "" + + for ; *index < len(lines); *index++ { + v := strings.TrimSpace(lines[*index]) + if len(v) == 0 { + continue + } + + fields := FieldsByAnySpace(v, 2) + securityAttr := strings.ToLower(fields[0]) + var value string + if len(fields) > 1 { + value = fields[1] + } + + for _, findTerm := range search { + if securityAttr == findTerm { + attrMap[securityAttr] = value + + break + } + } + + isExists, err := isExistsScope(securityAttr) + if err != nil { + return "", nil, err + } + + if isExists { + scopes[securityAttr[len(scopeAttrPrefix):]] = v[len(securityAttr):] + } + + if strings.HasPrefix(securityAttr, "@x-") { + // Add the custom attribute without the @ + extensions[securityAttr[1:]] = value + } + + // Not mandatory field + if securityAttr == descriptionAttr { + description = value + } + + // next securityDefinitions + if strings.Index(securityAttr, "@securitydefinitions.") == 0 { + // Go back to the previous line and break + *index-- + + break + } + } + + if len(attrMap) != len(search) { + return "", nil, fmt.Errorf("%s is %v required", context, search) + } + + scheme := &spec.SecurityScheme{} + key := getSecurityDefinitionKey(lines) + + switch attribute { + case secAPIKeyAttr: + scheme.Type = "apiKey" + scheme.In = attrMap[in] + scheme.Name = attrMap[name] + case secApplicationAttr: + scheme.Type = "oauth2" + scheme.In = attrMap[in] + scheme.Flows = spec.NewOAuthFlows() + scheme.Flows.Spec.ClientCredentials = spec.NewOAuthFlow() + scheme.Flows.Spec.ClientCredentials.Spec.TokenURL = attrMap[tokenURL] + + scheme.Flows.Spec.ClientCredentials.Spec.Scopes = make(map[string]string) + for k, v := range scopes { + scheme.Flows.Spec.ClientCredentials.Spec.Scopes[k] = v + } + case secImplicitAttr: + scheme.Type = "oauth2" + scheme.In = attrMap[in] + scheme.Flows = spec.NewOAuthFlows() + scheme.Flows.Spec.Implicit = spec.NewOAuthFlow() + scheme.Flows.Spec.Implicit.Spec.AuthorizationURL = attrMap[authorizationURL] + scheme.Flows.Spec.Implicit.Spec.Scopes = make(map[string]string) + for k, v := range scopes { + scheme.Flows.Spec.Implicit.Spec.Scopes[k] = v + } + case secPasswordAttr: + scheme.Type = "oauth2" + scheme.In = attrMap[in] + scheme.Flows = spec.NewOAuthFlows() + scheme.Flows.Spec.Password = spec.NewOAuthFlow() + scheme.Flows.Spec.Password.Spec.TokenURL = attrMap[tokenURL] + + scheme.Flows.Spec.Password.Spec.Scopes = make(map[string]string) + for k, v := range scopes { + scheme.Flows.Spec.Password.Spec.Scopes[k] = v + } + + case secAccessCodeAttr: + scheme.Type = "oauth2" + scheme.In = attrMap[in] + scheme.Flows = spec.NewOAuthFlows() + scheme.Flows.Spec.AuthorizationCode = spec.NewOAuthFlow() + scheme.Flows.Spec.AuthorizationCode.Spec.AuthorizationURL = attrMap[authorizationURL] + scheme.Flows.Spec.AuthorizationCode.Spec.TokenURL = attrMap[tokenURL] + } + + scheme.Description = description + + if scheme.Flows != nil && scheme.Flows.Extensions == nil && len(extensions) > 0 { + scheme.Flows.Extensions = make(map[string]interface{}) + } + + for k, v := range extensions { + scheme.Flows.Extensions[k] = v + } + + return key, scheme, nil +} + +func getSecurityDefinitionKey(lines []string) string { + for _, line := range lines { + if strings.HasPrefix(strings.ToLower(line), "@securitydefinitions") { + splittedLine := strings.Split(line, " ") + return splittedLine[len(splittedLine)-1] + } + } + + return "" +} + +// ParseRouterAPIInfo parses router api info for given astFile. +func (parser *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { + for _, astDescription := range fileInfo.File.Decls { + if (fileInfo.ParseFlag & ParseOperations) == ParseNone { + continue + } + + astDeclaration, ok := astDescription.(*ast.FuncDecl) + if !ok || astDeclaration.Doc == nil || astDeclaration.Doc.List == nil { + continue + } + + if parser.matchTags(astDeclaration.Doc.List) && + matchExtension(parser.parseExtension, astDeclaration.Doc.List) { + // for per 'function' comment, create a new 'Operation' object + operation := NewOperationV3(parser, SetCodeExampleFilesDirectoryV3(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) + } + } + err := processRouterOperationV3(parser, operation) + if err != nil { + return err + } + } + } + + return nil +} + +func processRouterOperationV3(p *Parser, o *OperationV3) error { + for _, routeProperties := range o.RouterProperties { + var ( + pathItem *spec.RefOrSpec[spec.Extendable[spec.PathItem]] + ok bool + ) + + pathItem, ok = p.openAPI.Paths.Spec.Paths[routeProperties.Path] + if !ok { + pathItem = &spec.RefOrSpec[spec.Extendable[spec.PathItem]]{ + Spec: &spec.Extendable[spec.PathItem]{ + Spec: &spec.PathItem{}, + }, + } + } + + op := refRouteMethodOpV3(pathItem.Spec.Spec, routeProperties.HTTPMethod) + + // check if we already have an operation for this path and method + if *op != nil { + err := fmt.Errorf("route %s %s is declared multiple times", routeProperties.HTTPMethod, routeProperties.Path) + if p.Strict { + return err + } + + p.debug.Printf("warning: %s\n", err) + } + + *op = &o.Operation + + p.openAPI.Paths.Spec.Paths[routeProperties.Path] = pathItem + } + + return nil +} + +func refRouteMethodOpV3(item *spec.PathItem, method string) **spec.Operation { + switch method { + case http.MethodGet: + if item.Get == nil { + item.Get = &spec.Extendable[spec.Operation]{} + } + return &item.Get.Spec + case http.MethodPost: + if item.Post == nil { + item.Post = &spec.Extendable[spec.Operation]{} + } + return &item.Post.Spec + case http.MethodDelete: + if item.Delete == nil { + item.Delete = &spec.Extendable[spec.Operation]{} + } + return &item.Delete.Spec + case http.MethodPut: + if item.Put == nil { + item.Put = &spec.Extendable[spec.Operation]{} + } + return &item.Put.Spec + case http.MethodPatch: + if item.Patch == nil { + item.Patch = &spec.Extendable[spec.Operation]{} + } + return &item.Patch.Spec + case http.MethodHead: + if item.Head == nil { + item.Head = &spec.Extendable[spec.Operation]{} + } + return &item.Head.Spec + case http.MethodOptions: + if item.Options == nil { + item.Options = &spec.Extendable[spec.Operation]{} + } + return &item.Options.Spec + default: + return nil + } +} + +func (p *Parser) getTypeSchemaV3(typeName string, file *ast.File, ref bool) (*spec.RefOrSpec[spec.Schema], error) { + if override, ok := p.Overrides[typeName]; ok { + p.debug.Printf("Override detected for %s: using %s instead", typeName, override) + schema, err := parseObjectSchemaV3(p, override, file) + if err != nil { + return nil, err + } + + return schema, nil + + } + + if IsInterfaceLike(typeName) { + return spec.NewSchemaSpec(), nil + } + + if IsGolangPrimitiveType(typeName) { + return PrimitiveSchemaV3(TransToValidSchemeType(typeName)), nil + } + + schemaType, err := convertFromSpecificToPrimitive(typeName) + if err == nil { + return PrimitiveSchemaV3(schemaType), nil + } + + typeSpecDef := p.packages.FindTypeSpec(typeName, file) + if typeSpecDef == nil { + p.packages.FindTypeSpec(typeName, file) // uncomment for debugging + return nil, fmt.Errorf("cannot find type definition: %s", typeName) + } + + if override, ok := p.Overrides[typeSpecDef.FullPath()]; ok { + if override == "" { + p.debug.Printf("Override detected for %s: ignoring", typeSpecDef.FullPath()) + + return nil, ErrSkippedField + } + + p.debug.Printf("Override detected for %s: using %s instead", typeSpecDef.FullPath(), override) + + separator := strings.LastIndex(override, ".") + if separator == -1 { + // treat as a swaggertype tag + parts := strings.Split(override, ",") + return BuildCustomSchemaV3(parts) + } + + typeSpecDef = p.packages.findTypeSpec(override[0:separator], override[separator+1:]) + } + + schema, ok := p.parsedSchemasV3[typeSpecDef] + if !ok { + var err error + + schema, err = p.ParseDefinitionV3(typeSpecDef) + if err != nil { + if err == ErrRecursiveParseStruct && ref { + return p.getRefTypeSchemaV3(typeSpecDef, schema), nil + } + return nil, err + } + } + + if ref { + if IsComplexSchemaV3(schema) { + return p.getRefTypeSchemaV3(typeSpecDef, schema), nil + } + + // if it is a simple schema, just return a copy + newSchema := *schema.Schema + return spec.NewRefOrSpec(nil, &newSchema), nil + } + + return spec.NewRefOrSpec(nil, schema.Schema), nil +} + +// ParseDefinitionV3 parses given type spec that corresponds to the type under +// given name and package, and populates swagger schema definitions registry +// with a schema for the given type +func (p *Parser) ParseDefinitionV3(typeSpecDef *TypeSpecDef) (*SchemaV3, error) { + typeName := typeSpecDef.TypeName() + schema, found := p.parsedSchemasV3[typeSpecDef] + if found { + p.debug.Printf("Skipping '%s', already parsed.", typeName) + + return schema, nil + } + + if p.isInStructStack(typeSpecDef) { + p.debug.Printf("Skipping '%s', recursion detected.", typeName) + + return &SchemaV3{ + Name: typeName, + PkgPath: typeSpecDef.PkgPath, + Schema: PrimitiveSchemaV3(OBJECT).Spec, + }, + ErrRecursiveParseStruct + } + + p.structStack = append(p.structStack, typeSpecDef) + + p.debug.Printf("Generating %s", typeName) + + definition, err := p.parseTypeExprV3(typeSpecDef.File, typeSpecDef.TypeSpec.Type, false) + if err != nil { + p.debug.Printf("Error parsing type definition '%s': %s", typeName, err) + return nil, err + } + + if definition.Spec.Description == "" { + fillDefinitionDescriptionV3(definition.Spec, typeSpecDef.File, typeSpecDef) + } + + if len(typeSpecDef.Enums) > 0 { + var varNames []string + var enumComments = make(map[string]string) + for _, value := range typeSpecDef.Enums { + definition.Spec.Enum = append(definition.Spec.Enum, value.Value) + varNames = append(varNames, value.key) + if len(value.Comment) > 0 { + enumComments[value.key] = value.Comment + } + } + + if definition.Spec.Extensions == nil { + definition.Spec.Extensions = make(map[string]any) + } + + definition.Spec.Extensions[enumVarNamesExtension] = varNames + if len(enumComments) > 0 { + definition.Spec.Extensions[enumCommentsExtension] = enumComments + } + } + + sch := SchemaV3{ + Name: typeName, + PkgPath: typeSpecDef.PkgPath, + Schema: definition.Spec, + } + p.parsedSchemasV3[typeSpecDef] = &sch + + // update an empty schema as a result of recursion + s2, found := p.outputSchemasV3[typeSpecDef] + if found { + p.openAPI.Components.Spec.Schemas[s2.Name] = definition + } + + return &sch, nil +} + +// fillDefinitionDescription additionally fills fields in definition (spec.Schema) +// TODO: If .go file contains many types, it may work for a long time +func fillDefinitionDescriptionV3(definition *spec.Schema, file *ast.File, typeSpecDef *TypeSpecDef) { + for _, astDeclaration := range file.Decls { + generalDeclaration, ok := astDeclaration.(*ast.GenDecl) + if !ok || generalDeclaration.Tok != token.TYPE { + continue + } + + for _, astSpec := range generalDeclaration.Specs { + typeSpec, ok := astSpec.(*ast.TypeSpec) + if !ok || typeSpec != typeSpecDef.TypeSpec { + continue + } + + definition.Description = + extractDeclarationDescription(typeSpec.Doc, typeSpec.Comment, generalDeclaration.Doc) + } + } +} + +// parseTypeExprV3 parses given type expression that corresponds to the type under +// given name and package, and returns swagger schema for it. +func (p *Parser) parseTypeExprV3(file *ast.File, typeExpr ast.Expr, ref bool) (*spec.RefOrSpec[spec.Schema], error) { + const errMessage = "parse type expression v3" + + switch expr := typeExpr.(type) { + // type Foo interface{} + case *ast.InterfaceType: + return spec.NewSchemaSpec(), nil + + // type Foo struct {...} + case *ast.StructType: + return p.parseStructV3(file, expr.Fields) + + // type Foo Baz + case *ast.Ident: + result, err := p.getTypeSchemaV3(expr.Name, file, true) + if err != nil { + return nil, errors.Wrap(err, errMessage) + } + + return result, nil + // type Foo *Baz + case *ast.StarExpr: + return p.parseTypeExprV3(file, expr.X, ref) + + // type Foo pkg.Bar + case *ast.SelectorExpr: + if xIdent, ok := expr.X.(*ast.Ident); ok { + result, err := p.getTypeSchemaV3(fullTypeName(xIdent.Name, expr.Sel.Name), file, ref) + if err != nil { + return nil, errors.Wrap(err, errMessage) + } + + return result, nil + } + // type Foo []Baz + case *ast.ArrayType: + itemSchema, err := p.parseTypeExprV3(file, expr.Elt, true) + if err != nil { + return nil, err + } + + if itemSchema == nil { + schema := &spec.Schema{} + schema.Type = spec.NewSingleOrArray(ARRAY) + schema.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + p.debug.Printf("Creating array with empty item schema %v", expr.Elt) + + return spec.NewRefOrSpec(nil, schema), nil + } + + result := &spec.Schema{} + result.Type = spec.NewSingleOrArray(ARRAY) + result.Items = spec.NewBoolOrSchema(false, itemSchema) + + return spec.NewRefOrSpec(nil, result), nil + // type Foo map[string]Bar + case *ast.MapType: + if _, ok := expr.Value.(*ast.InterfaceType); ok { + result := &spec.Schema{} + result.AdditionalProperties = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + result.Type = spec.NewSingleOrArray(OBJECT) + + return spec.NewRefOrSpec(nil, result), nil + } + + schema, err := p.parseTypeExprV3(file, expr.Value, true) + if err != nil { + return nil, err + } + + result := &spec.Schema{} + result.AdditionalProperties = spec.NewBoolOrSchema(false, schema) + result.Type = spec.NewSingleOrArray(OBJECT) + + return spec.NewRefOrSpec(nil, result), nil + case *ast.FuncType: + return nil, ErrFuncTypeField + // ... + } + + return p.parseGenericTypeExprV3(file, typeExpr) +} + +func (p *Parser) parseStructV3(file *ast.File, fields *ast.FieldList) (*spec.RefOrSpec[spec.Schema], error) { + required, properties := make([]string, 0), make(map[string]*spec.RefOrSpec[spec.Schema]) + + for _, field := range fields.List { + fieldProps, requiredFromAnon, err := p.parseStructFieldV3(file, field) + if err != nil { + if err == ErrFuncTypeField || err == ErrSkippedField { + continue + } + + return nil, err + } + + if len(fieldProps) == 0 { + continue + } + + required = append(required, requiredFromAnon...) + + for k, v := range fieldProps { + properties[k] = v + } + } + + sort.Strings(required) + + result := spec.NewSchemaSpec() + result.Spec.Type = spec.NewSingleOrArray(OBJECT) + result.Spec.Properties = properties + result.Spec.Required = required + + return result, nil +} + +func (p *Parser) parseStructFieldV3(file *ast.File, field *ast.Field) (map[string]*spec.RefOrSpec[spec.Schema], []string, error) { + if field.Tag != nil { + skip, ok := reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", "")).Lookup("swaggerignore") + if ok && strings.EqualFold(skip, "true") { + return nil, nil, nil + } + } + + ps := p.fieldParserFactoryV3(p, field) + + if ps.ShouldSkip() { + return nil, nil, nil + } + + fieldName, err := ps.FieldName() + if err != nil { + return nil, nil, err + } + + if fieldName == "" { + typeName, err := getFieldType(file, field.Type, nil) + if err != nil { + return nil, nil, err + } + + schema, err := p.getTypeSchemaV3(typeName, file, false) + if err != nil { + return nil, nil, err + } + + if len(schema.Spec.Type) > 0 && schema.Spec.Type[0] == OBJECT { + if len(schema.Spec.Properties) == 0 { + return nil, nil, nil + } + + properties := make(map[string]*spec.RefOrSpec[spec.Schema]) + for k, v := range schema.Spec.Properties { + properties[k] = v + } + + return properties, schema.Spec.Required, nil + } + // for alias type of non-struct types ,such as array,map, etc. ignore field tag. + return map[string]*spec.RefOrSpec[spec.Schema]{ + typeName: schema, + }, nil, nil + + } + + schema, err := ps.CustomSchema() + if err != nil { + return nil, nil, err + } + + if schema == nil { + typeName, err := getFieldType(file, field.Type, nil) + if err == nil { + // named type + schema, err = p.getTypeSchemaV3(typeName, file, true) + if err != nil { + return nil, nil, err + } + + } else { + // unnamed type + parsedSchema, err := p.parseTypeExprV3(file, field.Type, false) + if err != nil { + return nil, nil, err + } + + schema = parsedSchema + } + } + + err = ps.ComplementSchema(schema) + if err != nil { + return nil, nil, err + } + + var tagRequired []string + + required, err := ps.IsRequired() + if err != nil { + return nil, nil, err + } + + if required { + tagRequired = append(tagRequired, fieldName) + } + + if formName := ps.FormName(); len(formName) > 0 { + if schema.Spec.Extensions == nil { + schema.Spec.Extensions = make(map[string]any) + } + schema.Spec.Extensions[formTag] = formName + } + + return map[string]*spec.RefOrSpec[spec.Schema]{fieldName: schema}, tagRequired, nil +} + +func (p *Parser) getRefTypeSchemaV3(typeSpecDef *TypeSpecDef, schema *SchemaV3) *spec.RefOrSpec[spec.Schema] { + _, ok := p.outputSchemasV3[typeSpecDef] + if !ok { + if p.openAPI.Components.Spec.Schemas == nil { + p.openAPI.Components.Spec.Schemas = make(map[string]*spec.RefOrSpec[spec.Schema]) + } + + p.openAPI.Components.Spec.Schemas[schema.Name] = spec.NewSchemaSpec() + + if schema.Schema != nil { + p.openAPI.Components.Spec.Schemas[schema.Name] = spec.NewRefOrSpec(nil, schema.Schema) + } + + p.outputSchemasV3[typeSpecDef] = schema + } + + refSchema := RefSchemaV3(schema.Name) + + return refSchema +} + +// GetSchemaTypePath get path of schema type. +func (parser *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], depth int) []string { + if schema == nil || depth == 0 { + return nil + } + + name := "" + if schema.Ref != nil { + name = schema.Ref.Ref + } + + if name != "" { + if pos := strings.LastIndexByte(name, '/'); pos >= 0 { + name = name[pos+1:] + if schema, ok := parser.openAPI.Components.Spec.Schemas[name]; ok { + return parser.GetSchemaTypePathV3(schema, depth) + } + } + + return nil + } + + if schema.Spec != nil && len(schema.Spec.Type) > 0 { + switch schema.Spec.Type[0] { + case ARRAY: + depth-- + + s := []string{schema.Spec.Type[0]} + + return append(s, parser.GetSchemaTypePathV3(schema.Spec.Items.Schema, depth)...) + case OBJECT: + if schema.Spec.AdditionalProperties != nil && schema.Spec.AdditionalProperties.Schema != nil { + // for map + depth-- + + s := []string{schema.Spec.Type[0]} + + return append(s, parser.GetSchemaTypePathV3(schema.Spec.AdditionalProperties.Schema, depth)...) + } + } + + return []string{schema.Spec.Type[0]} + } + + println("found schema with no Type, returning any") + return []string{ANY} +} + +func (p *Parser) getSchemaByRef(ref *spec.Ref) *spec.Schema { + searchString := strings.ReplaceAll(ref.Ref, "#/components/schemas/", "") + return p.openAPI.Components.Spec.Schemas[searchString].Spec +} diff --git a/parserv3_test.go b/parserv3_test.go new file mode 100644 index 000000000..990b656d4 --- /dev/null +++ b/parserv3_test.go @@ -0,0 +1,369 @@ +package swag + +import ( + "go/ast" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOverridesGetTypeSchemaV3(t *testing.T) { + t.Parallel() + + overrides := map[string]string{ + "sql.NullString": "string", + } + + p := New(SetOverrides(overrides)) + + t.Run("Override sql.NullString by string", func(t *testing.T) { + t.Parallel() + + s, err := p.getTypeSchemaV3("sql.NullString", nil, false) + if assert.NoError(t, err) { + assert.Truef(t, s.Spec.Type[0] == "string", "type sql.NullString should be overridden by string") + } + }) + + t.Run("Missing Override for sql.NullInt64", func(t *testing.T) { + t.Parallel() + + _, err := p.getTypeSchemaV3("sql.NullInt64", nil, false) + if assert.Error(t, err) { + assert.Equal(t, "cannot find type definition: sql.NullInt64", err.Error()) + } + }) +} + +func TestParserParseDefinitionV3(t *testing.T) { + p := New() + + // Parsing existing type + definition := &TypeSpecDef{ + PkgPath: "github.com/swagger/swag", + File: &ast.File{ + Name: &ast.Ident{ + Name: "swag", + }, + }, + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{ + Name: "Test", + }, + }, + } + + expected := &SchemaV3{} + p.parsedSchemasV3[definition] = expected + + schema, err := p.ParseDefinitionV3(definition) + assert.NoError(t, err) + assert.Equal(t, expected, schema) + + // Parsing *ast.FuncType + 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{}, + }, + } + _, err = p.ParseDefinitionV3(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.ParseDefinitionV3(definition) + assert.Error(t, err) + assert.Equal(t, "model.TestFuncDecl.Test", definition.TypeName()) +} + +func TestParserParseGeneralApiInfoV3(t *testing.T) { + t.Parallel() + + gopath := os.Getenv("GOPATH") + assert.NotNil(t, gopath) + + p := New(SetOpenAPIVersion(true)) + + err := p.ParseGeneralAPIInfo("testdata/v3/main.go") + assert.NoError(t, err) + + assert.Equal(t, "This is a sample server Petstore server.\nIt has a lot of beautiful features.", p.openAPI.Info.Spec.Description) + assert.Equal(t, "Swagger Example API", p.openAPI.Info.Spec.Title) + assert.Equal(t, "http://swagger.io/terms/", p.openAPI.Info.Spec.TermsOfService) + assert.Equal(t, "API Support", p.openAPI.Info.Spec.Contact.Spec.Name) + assert.Equal(t, "http://www.swagger.io/support", p.openAPI.Info.Spec.Contact.Spec.URL) + assert.Equal(t, "support@swagger.io", p.openAPI.Info.Spec.Contact.Spec.Email) + assert.Equal(t, "Apache 2.0", p.openAPI.Info.Spec.License.Spec.Name) + assert.Equal(t, "http://www.apache.org/licenses/LICENSE-2.0.html", p.openAPI.Info.Spec.License.Spec.URL) + assert.Equal(t, "1.0", p.openAPI.Info.Spec.Version) + + xLogo := map[string]interface{}(map[string]interface{}{"altText": "Petstore logo", "backgroundColor": "#FFFFFF", "url": "https://redocly.github.io/redoc/petstore-logo.png"}) + assert.Equal(t, xLogo, p.openAPI.Info.Extensions["x-logo"]) + assert.Equal(t, "marks values", p.openAPI.Info.Extensions["x-google-marks"]) + + endpoints := interface{}([]interface{}{map[string]interface{}{"allowCors": true, "name": "name.endpoints.environment.cloud.goog"}}) + assert.Equal(t, endpoints, p.openAPI.Info.Extensions["x-google-endpoints"]) + + assert.Equal(t, "OpenAPI", p.openAPI.ExternalDocs.Spec.Description) + assert.Equal(t, "https://swagger.io/resources/open-api", p.openAPI.ExternalDocs.Spec.URL) + + assert.Equal(t, 6, len(p.openAPI.Components.Spec.SecuritySchemes)) + + security := p.openAPI.Components.Spec.SecuritySchemes + assert.Equal(t, "basic", security["basic"].Spec.Spec.Scheme) + assert.Equal(t, "http", security["basic"].Spec.Spec.Type) + + assert.Equal(t, "apiKey", security["ApiKeyAuth"].Spec.Spec.Type) + assert.Equal(t, "Authorization", security["ApiKeyAuth"].Spec.Spec.Name) + assert.Equal(t, "header", security["ApiKeyAuth"].Spec.Spec.In) + assert.Equal(t, "some description", security["ApiKeyAuth"].Spec.Spec.Description) + + assert.Equal(t, "oauth2", security["OAuth2Application"].Spec.Spec.Type) + assert.Equal(t, "header", security["OAuth2Application"].Spec.Spec.In) + assert.Equal(t, "https://example.com/oauth/token", security["OAuth2Application"].Spec.Spec.Flows.Spec.ClientCredentials.Spec.TokenURL) + assert.Equal(t, 2, len(security["OAuth2Application"].Spec.Spec.Flows.Spec.ClientCredentials.Spec.Scopes)) + + assert.Equal(t, "oauth2", security["OAuth2Implicit"].Spec.Spec.Type) + assert.Equal(t, "header", security["OAuth2Implicit"].Spec.Spec.In) + assert.Equal(t, "https://example.com/oauth/authorize", security["OAuth2Implicit"].Spec.Spec.Flows.Spec.Implicit.Spec.AuthorizationURL) + assert.Equal(t, "some_audience.google.com", security["OAuth2Implicit"].Spec.Spec.Flows.Extensions["x-google-audiences"]) + + assert.Equal(t, "oauth2", security["OAuth2Password"].Spec.Spec.Type) + assert.Equal(t, "header", security["OAuth2Password"].Spec.Spec.In) + assert.Equal(t, "https://example.com/oauth/token", security["OAuth2Password"].Spec.Spec.Flows.Spec.Password.Spec.TokenURL) + + assert.Equal(t, "oauth2", security["OAuth2AccessCode"].Spec.Spec.Type) + assert.Equal(t, "header", security["OAuth2AccessCode"].Spec.Spec.In) + assert.Equal(t, "https://example.com/oauth/token", security["OAuth2AccessCode"].Spec.Spec.Flows.Spec.AuthorizationCode.Spec.TokenURL) +} + +func TestParser_ParseGeneralApiInfoExtensionsV3(t *testing.T) { + // should return an error because extension value is not a valid json + t.Run("Test invalid extension value", func(t *testing.T) { + t.Parallel() + + expected := "could not parse extension comment: annotation @x-google-endpoints need a valid json value. error: invalid character ':' after array element" + gopath := os.Getenv("GOPATH") + assert.NotNil(t, gopath) + + p := New(SetOpenAPIVersion(true)) + + err := p.ParseGeneralAPIInfo("testdata/v3/extensionsFail1.go") + if assert.Error(t, err) { + assert.Equal(t, expected, err.Error()) + } + }) + + // should return an error because extension don't have a value + t.Run("Test missing extension value", func(t *testing.T) { + t.Parallel() + + expected := "could not parse extension comment: annotation @x-google-endpoints need a value" + gopath := os.Getenv("GOPATH") + assert.NotNil(t, gopath) + + p := New(SetOpenAPIVersion(true)) + + err := p.ParseGeneralAPIInfo("testdata/v3/extensionsFail2.go") + if assert.Error(t, err) { + assert.Equal(t, expected, err.Error()) + } + }) +} + +func TestParserParseGeneralApiInfoWithOpsInSameFileV3(t *testing.T) { + t.Parallel() + + gopath := os.Getenv("GOPATH") + assert.NotNil(t, gopath) + + p := New(SetOpenAPIVersion(true)) + + err := p.ParseGeneralAPIInfo("testdata/single_file_api/main.go") + assert.NoError(t, err) + + assert.Equal(t, "This is a sample server Petstore server.\nIt has a lot of beautiful features.", p.openAPI.Info.Spec.Description) + assert.Equal(t, "Swagger Example API", p.openAPI.Info.Spec.Title) + assert.Equal(t, "http://swagger.io/terms/", p.openAPI.Info.Spec.TermsOfService) +} + +func TestParserParseGeneralAPIInfoMarkdownV3(t *testing.T) { + t.Parallel() + + p := New(SetMarkdownFileDirectory("testdata"), SetOpenAPIVersion(true)) + mainAPIFile := "testdata/markdown.go" + err := p.ParseGeneralAPIInfo(mainAPIFile) + assert.NoError(t, err) + + assert.Equal(t, "users", p.openAPI.Tags[0].Spec.Name) + assert.Equal(t, "Users Tag Markdown Description", p.openAPI.Tags[0].Spec.Description) + + p = New(SetOpenAPIVersion(true)) + + err = p.ParseGeneralAPIInfo(mainAPIFile) + assert.Error(t, err) +} + +func TestParserParseGeneralApiInfoFailedV3(t *testing.T) { + t.Parallel() + + gopath := os.Getenv("GOPATH") + assert.NotNil(t, gopath) + p := New(SetOpenAPIVersion(true)) + assert.Error(t, p.ParseGeneralAPIInfo("testdata/noexist.go")) +} + +func TestParserParseGeneralAPIInfoCollectionFormatV3(t *testing.T) { + t.Parallel() + + parser := New(SetOpenAPIVersion(true)) + assert.NoError(t, parser.parseGeneralAPIInfoV3([]string{ + "@query.collection.format csv", + })) + assert.Equal(t, parser.collectionFormatInQuery, "csv") + + assert.NoError(t, parser.parseGeneralAPIInfoV3([]string{ + "@query.collection.format tsv", + })) + assert.Equal(t, parser.collectionFormatInQuery, "tsv") +} + +func TestParserParseGeneralAPITagGroupsV3(t *testing.T) { + t.Parallel() + + parser := New(SetOpenAPIVersion(true)) + assert.NoError(t, parser.parseGeneralAPIInfoV3([]string{ + "@x-tagGroups [{\"name\":\"General\",\"tags\":[\"lanes\",\"video-recommendations\"]}]", + })) + + expected := []interface{}{map[string]interface{}{"name": "General", "tags": []interface{}{"lanes", "video-recommendations"}}} + assert.Equal(t, expected, parser.openAPI.Info.Extensions["x-tagGroups"]) +} + +func TestParserParseGeneralAPITagDocsV3(t *testing.T) { + t.Parallel() + + parser := New(SetOpenAPIVersion(true)) + assert.Error(t, parser.parseGeneralAPIInfoV3([]string{ + "@tag.name Test", + "@tag.docs.description Best example documentation"})) + + parser = New(SetOpenAPIVersion(true)) + err := parser.parseGeneralAPIInfoV3([]string{ + "@tag.name test", + "@tag.description A test Tag", + "@tag.docs.url https://example.com", + "@tag.docs.description Best example documentation"}) + assert.NoError(t, err) + + assert.Equal(t, "test", parser.openAPI.Tags[0].Spec.Name) + assert.Equal(t, "A test Tag", parser.openAPI.Tags[0].Spec.Description) + assert.Equal(t, "https://example.com", parser.openAPI.Tags[0].Spec.ExternalDocs.Spec.URL) + assert.Equal(t, "Best example documentation", parser.openAPI.Tags[0].Spec.ExternalDocs.Spec.Description) +} + +func TestGetAllGoFileInfoV3(t *testing.T) { + t.Parallel() + + searchDir := "testdata/pet" + + p := New(SetOpenAPIVersion(true)) + err := p.getAllGoFileInfo("testdata", searchDir) + + assert.NoError(t, err) + assert.Equal(t, 2, len(p.packages.files)) +} + +func TestParser_ParseTypeV3(t *testing.T) { + t.Parallel() + + searchDir := "testdata/v3/simple/" + + p := New(SetOpenAPIVersion(true)) + err := p.getAllGoFileInfo("testdata", searchDir) + assert.NoError(t, err) + + _, err = p.packages.ParseTypes() + + assert.NoError(t, err) + assert.NotNil(t, p.packages.uniqueDefinitions["api.Pet3"]) + assert.NotNil(t, p.packages.uniqueDefinitions["web.Pet"]) + assert.NotNil(t, p.packages.uniqueDefinitions["web.Pet2"]) +} + +func TestParsePet(t *testing.T) { + t.Parallel() + + searchDir := "testdata/v3/pet" + + p := New(SetOpenAPIVersion(true)) + p.PropNamingStrategy = PascalCase + + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + + schemas := p.openAPI.Components.Spec.Schemas + assert.NotNil(t, schemas) + + tagSchema := schemas["web.Tag"].Spec + assert.Equal(t, 2, len(tagSchema.Properties)) + assert.Equal(t, typeInteger, tagSchema.Properties["id"].Spec.Type) + assert.Equal(t, typeString, tagSchema.Properties["name"].Spec.Type) + + petSchema := schemas["web.Pet"].Spec + assert.NotNil(t, petSchema) + assert.Equal(t, 8, len(petSchema.Properties)) + assert.Equal(t, typeInteger, petSchema.Properties["id"].Spec.Type) + assert.Equal(t, typeString, petSchema.Properties["name"].Spec.Type) + +} + +func TestParseSimpleApiV3(t *testing.T) { + t.Parallel() + + searchDir := "testdata/v3/simple" + p := New(SetOpenAPIVersion(true)) + p.PropNamingStrategy = PascalCase + + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + + paths := p.openAPI.Paths.Spec.Paths + assert.Equal(t, 14, len(paths)) + + path := paths["/testapi/get-string-by-int/{some_id}"].Spec.Spec.Get.Spec + assert.Equal(t, "get string by ID", path.Description) + assert.Equal(t, "Add a new pet to the store", path.Summary) + assert.Equal(t, "get-string-by-int", path.OperationID) + + response := path.Responses.Spec.Response["200"] + assert.Equal(t, "ok", response.Spec.Spec.Description) + + //TODO add asserts +} diff --git a/schemav3.go b/schemav3.go new file mode 100644 index 000000000..f00a2df93 --- /dev/null +++ b/schemav3.go @@ -0,0 +1,141 @@ +package swag + +import ( + "errors" + + "github.com/sv-tools/openapi/spec" +) + +// PrimitiveSchemaV3 build a primitive schema. +func PrimitiveSchemaV3(refType string) *spec.RefOrSpec[spec.Schema] { + result := spec.NewSchemaSpec() + result.Spec.Type = spec.SingleOrArray[string]{refType} + + return result +} + +// IsComplexSchemaV3 whether a schema is complex and should be a ref schema +func IsComplexSchemaV3(schema *SchemaV3) bool { + // a enum type should be complex + if len(schema.Enum) > 0 { + return true + } + + // a deep array type is complex, how to determine deep? here more than 2 ,for example: [][]object,[][][]int + if len(schema.Type) > 2 { + return true + } + + //Object included, such as Object or []Object + for _, st := range schema.Type { + if st == OBJECT { + return true + } + } + return false +} + +// RefSchemaV3 build a reference schema. +func RefSchemaV3(refType string) *spec.RefOrSpec[spec.Schema] { + return spec.NewRefOrSpec[spec.Schema](spec.NewRef("#/components/schemas/"+refType), nil) +} + +// BuildCustomSchemaV3 build custom schema specified by tag swaggertype. +func BuildCustomSchemaV3(types []string) (*spec.RefOrSpec[spec.Schema], error) { + if len(types) == 0 { + return nil, nil + } + + switch types[0] { + case PRIMITIVE: + if len(types) == 1 { + return nil, errors.New("need primitive type after primitive") + } + + return BuildCustomSchemaV3(types[1:]) + case ARRAY: + if len(types) == 1 { + return nil, errors.New("need array item type after array") + } + + schema, err := BuildCustomSchemaV3(types[1:]) + if err != nil { + return nil, err + } + + // TODO: check if this is correct + result := spec.NewSchemaSpec() + result.Spec.Type = []string{"array"} + result.Spec.AdditionalProperties = spec.NewBoolOrSchema(true, schema) + + return result, nil + case OBJECT: + if len(types) == 1 { + return PrimitiveSchemaV3(types[0]), nil + } + + schema, err := BuildCustomSchemaV3(types[1:]) + if err != nil { + return nil, err + } + + result := spec.NewSchemaSpec() + result.Spec.AdditionalProperties = spec.NewBoolOrSchema(true, schema) + result.Spec.Type = spec.NewSingleOrArray("object") + + return result, nil + default: + err := CheckSchemaType(types[0]) + if err != nil { + return nil, err + } + + return PrimitiveSchemaV3(types[0]), nil + } +} + +// TransToValidCollectionFormatV3 determine valid collection format. +func TransToValidCollectionFormatV3(format, in string) string { + switch in { + case "query": + switch format { + case "form", "spaceDelimited", "pipeDelimited", "deepObject": + return format + case "ssv": + return "spaceDelimited" + case "pipes": + return "pipe" + case "multi": + return "form" + case "csv": + return "form" + default: + return "" + } + case "path": + switch format { + case "matrix", "label", "simple": + return format + case "csv": + return "simple" + default: + return "" + } + case "header": + switch format { + case "form", "simple": + return format + case "csv": + return "simple" + default: + return "" + } + case "cookie": + switch format { + case "form": + return format + } + } + + return "" +} diff --git a/testdata/code_examples/example.json b/testdata/code_examples/example.json index 26e1cef56..a093daa66 100644 --- a/testdata/code_examples/example.json +++ b/testdata/code_examples/example.json @@ -1,4 +1,6 @@ -{ - "lang": "JavaScript", - "source": "console.log('Hello World');" -} \ No newline at end of file +[ + { + "lang": "JavaScript", + "source": "console.log('Hello World');" + } +] diff --git a/testdata/generics_property/api/api.go b/testdata/generics_property/api/api.go index e68938f81..2ef9198c1 100644 --- a/testdata/generics_property/api/api.go +++ b/testdata/generics_property/api/api.go @@ -1,9 +1,10 @@ package api import ( + "net/http" + "github.com/swaggo/swag/testdata/generics_property/types" "github.com/swaggo/swag/testdata/generics_property/web" - "net/http" ) type NestedResponse struct { diff --git a/testdata/v3/extensionsFail1.go b/testdata/v3/extensionsFail1.go new file mode 100644 index 000000000..59a0cf989 --- /dev/null +++ b/testdata/v3/extensionsFail1.go @@ -0,0 +1,9 @@ +package main + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @description It has a lot of beautiful features. +// @termsOfService http://swagger.io/terms/ + +// @x-google-endpoints ["name":"name.endpoints.environment.cloud.goog","allowCors":true}] diff --git a/testdata/v3/extensionsFail2.go b/testdata/v3/extensionsFail2.go new file mode 100644 index 000000000..5618b19fd --- /dev/null +++ b/testdata/v3/extensionsFail2.go @@ -0,0 +1,9 @@ +package main + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @description It has a lot of beautiful features. +// @termsOfService http://swagger.io/terms/ + +// @x-google-endpoints diff --git a/testdata/v3/main.go b/testdata/v3/main.go new file mode 100644 index 000000000..de93578c1 --- /dev/null +++ b/testdata/v3/main.go @@ -0,0 +1,64 @@ +package main + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @description It has a lot of beautiful features. +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host petstore.swagger.io +// @BasePath /v2 +// @schemes http https +// @securityDefinitions.basic BasicAuth + +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name Authorization +// @description some description + +// @securitydefinitions.oauth2.application OAuth2Application +// @tokenUrl https://example.com/oauth/token +// @in header +// @name name +// @scope.write Grants write access +// @scope.admin Grants read and write access to administrative information + +// @securitydefinitions.oauth2.implicit OAuth2Implicit +// @authorizationurl https://example.com/oauth/authorize +// @in header +// @scope.write Grant +// @name names write access +// @scope.admin Grants read and write access to administrative information +// @x-google-audiences some_audience.google.com + +// @securitydefinitions.oauth2.password OAuth2Password +// @tokenUrl https://example.com/oauth/token +// @in header +// @name name +// @scope.read Grants read access +// @scope.write Grants write access +// @scope.admin Grants read and write access to administrative information + +// @securitydefinitions.oauth2.accessCode OAuth2AccessCode +// @tokenUrl https://example.com/oauth/token +// @authorizationurl https://example.com/oauth/authorize +// @scope.admin Grants read and write access to administrative information +// @x-tokenname id_token +// @in header +// @name name + +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api + +// @x-google-endpoints [{"name":"name.endpoints.environment.cloud.goog","allowCors":true}] +// @x-google-marks "marks values" +// @x-logo {"url":"https://redocly.github.io/redoc/petstore-logo.png", "altText": "Petstore logo", "backgroundColor": "#FFFFFF"} + +func main() {} diff --git a/testdata/v3/pet/main.go b/testdata/v3/pet/main.go new file mode 100644 index 000000000..90182a2e6 --- /dev/null +++ b/testdata/v3/pet/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/v3/pet/web" +) + +// @title Swagger Petstore +// @version 1.0 +// @description This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key 'special-key' to test the authorization filters. +// @termsOfService http://swagger.io/terms/ + +// @contact.email apiteam@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +func main() { + http.HandleFunc("/testapi/pets", web.GetPets) +} diff --git a/testdata/v3/pet/web/handler.go b/testdata/v3/pet/web/handler.go new file mode 100644 index 000000000..0945e71d2 --- /dev/null +++ b/testdata/v3/pet/web/handler.go @@ -0,0 +1,38 @@ +package web + +import "net/http" + +type Tag struct { + ID int `json:"id"` + Name string `json:"name"` +} + +type Pet struct { + ID int `json:"id" example:"1"` + Category struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"category_name"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"` + SmallCategory struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"detail_category_name"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"` + } `json:"small_category"` + } `json:"category"` + Name string `json:"name" example:"poti"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"` + Tags []Tag `json:"tags"` + Status string `json:"status"` + Price float32 `json:"price" example:"3.25"` + IsAlive bool `json:"is_alive" example:"true"` +} + +// @Summary Get all pets +// @Description get all pets +// @ID get-pets +// @Success 200 {object} []web.Pet "ok" +// @Router /testapi/pets [get] +func GetPets(w http.ResponseWriter, r *http.Request) { + _ = Cross{} + //write your code +} diff --git a/testdata/v3/simple/api/api.go b/testdata/v3/simple/api/api.go new file mode 100644 index 000000000..324385e58 --- /dev/null +++ b/testdata/v3/simple/api/api.go @@ -0,0 +1,140 @@ +package api + +import ( + "net/http" + + . "github.com/swaggo/swag/testdata/v3/simple/cross" + _ "github.com/swaggo/swag/testdata/v3/simple/web" +) + +// @Summary Add a new pet to the store +// @Description get string by ID +// @ID get-string-by-int +// @Accept json +// @Produce json +// @Param some_id path int true "Some ID" Format(int64) +// @Param some_id body web.Pet true "Some ID" +// @Success 200 {string} string "ok" +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 404 {object} web.APIError "Can not find ID" +// @Router /testapi/get-string-by-int/{some_id} [get] +func GetStringByInt(w http.ResponseWriter, r *http.Request) { + _ = Cross{} + //write your code +} + +// @Description get struct array by ID +// @ID get-struct-array-by-string +// @Accept json +// @Produce json +// @Param some_id path string true "Some ID" +// @Param category query int true "Category" Enums(1, 2, 3) +// @Param offset query int true "Offset" Minimum(0) default(0) +// @Param limit query int true "Limit" Maximum(50) default(10) +// @Param q query string true "q" Minlength(1) Maxlength(50) default("") +// @Success 200 {string} string "ok" +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 404 {object} web.APIError "Can not find ID" +// @Security ApiKeyAuth +// @Security BasicAuth +// @Security OAuth2Application[write] +// @Security OAuth2Implicit[read, admin] +// @Security OAuth2AccessCode[read] +// @Security OAuth2Password[admin] +// @Security OAuth2Implicit[read, write] || Firebase +// @Router /testapi/get-struct-array-by-string/{some_id} [get] +func GetStructArrayByString(w http.ResponseWriter, r *http.Request) { + //write your code +} + +// @Summary Upload file +// @Description Upload file +// @ID file.upload +// @Accept multipart/form-data +// @Produce json +// @Param file formData file true "this is a test file" +// @Success 200 {string} string "ok" +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 401 {array} string +// @Failure 404 {object} web.APIError "Can not find ID" +// @Failure 403 {object} Cross "cross" +// @Router /file/upload [post] +func Upload(w http.ResponseWriter, r *http.Request) { + //write your code +} + +// @Summary use Anonymous field +// @Success 200 {object} web.RevValue "ok" +// @Router /AnonymousField [get] +func AnonymousField() { + +} + +// @Summary use pet2 +// @Success 200 {object} web.Pet2 "ok" +// @Router /Pet2 [get] +func Pet2() { + +} + +// @Summary Use IndirectRecursiveTest +// @Success 200 {object} web.IndirectRecursiveTest +// @Router /IndirectRecursiveTest [get] +func IndirectRecursiveTest() { +} + +// @Summary Use Tags +// @Success 200 {object} web.Tags +// @Router /Tags [get] +func Tags() { +} + +// @Summary Use CrossAlias +// @Success 200 {object} web.CrossAlias +// @Router /CrossAlias [get] +func CrossAlias() { +} + +// @Summary Use AnonymousStructArray +// @Success 200 {object} web.AnonymousStructArray +// @Router /AnonymousStructArray [get] +func AnonymousStructArray() { +} + +type Pet3 struct { + ID int `json:"id"` +} + +// @Success 200 {object} web.Pet5a "ok" +// @Router /GetPet5a [options] +func GetPet5a() { + +} + +// @Success 200 {object} web.Pet5b "ok" +// @Router /GetPet5b [head] +func GetPet5b() { + +} + +// @Success 200 {object} web.Pet5c "ok" +// @Router /GetPet5c [patch] +func GetPet5c() { + +} + +type SwagReturn []map[string]string + +// @Success 200 {object} api.SwagReturn "ok" +// @Router /GetPet6MapString [get] +func GetPet6MapString() { + +} + +// @Success 200 {object} api.GetPet6FunctionScopedResponse.response "ok" +// @Router /GetPet6FunctionScopedResponse [get] +func GetPet6FunctionScopedResponse() { + type response struct { + Name string + } +} diff --git a/testdata/v3/simple/cross/test.go b/testdata/v3/simple/cross/test.go new file mode 100644 index 000000000..540e53f4f --- /dev/null +++ b/testdata/v3/simple/cross/test.go @@ -0,0 +1,6 @@ +package cross + +type Cross struct { + Array []string + String string +} diff --git a/testdata/v3/simple/main.go b/testdata/v3/simple/main.go new file mode 100644 index 000000000..a7cd1447a --- /dev/null +++ b/testdata/v3/simple/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/v3/simple/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host petstore.swagger.io +// @BasePath /v2 +func main() { + http.HandleFunc("/testapi/get-string-by-int/", api.GetStringByInt) + http.HandleFunc("/testapi/get-struct-array-by-string/", api.GetStructArrayByString) + http.HandleFunc("/testapi/upload", api.Upload) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/v3/simple/web/handler.go b/testdata/v3/simple/web/handler.go new file mode 100644 index 000000000..546fe322c --- /dev/null +++ b/testdata/v3/simple/web/handler.go @@ -0,0 +1,101 @@ +package web + +import ( + "time" + + "github.com/gofrs/uuid" + "github.com/shopspring/decimal" + "github.com/swaggo/swag/testdata/v3/simple/cross" +) + +type Pet struct { + ID int `json:"id" example:"1" format:"int64" readonly:"true"` + Category struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"category_name"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg" format:"url"` + SmallCategory struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"detail_category_name" binding:"required" minLength:"4" maxLength:"16"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"` + } `json:"small_category"` + } `json:"category"` + Name string `json:"name" example:"poti" binding:"required"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg" binding:"required"` + Tags []Tag `json:"tags"` + Pets *[]Pet2 `json:"pets"` + Pets2 []*Pet2 `json:"pets2"` + Status string `json:"status" enums:"healthy,ill"` + Price float32 `json:"price" example:"3.25" minimum:"1.0" maximum:"1000" multipleOf:"0.01"` + IsAlive bool `json:"is_alive" example:"true" default:"true"` + Data interface{} `json:"data"` + Hidden string `json:"-"` + UUID uuid.UUID `json:"uuid"` + Decimal decimal.Decimal `json:"decimal"` + IntArray []int `json:"int_array" example:"1,2"` + StringMap map[string]string `json:"string_map" example:"key1:value,key2:value2"` + EnumArray []int `json:"enum_array" enums:"1,2,3,5,7"` + FoodTypes []string `json:"food_types" swaggertype:"array,integer" enums:"0,1,2" x-enum-varnames:"Wet,Dry,Raw" extensions:"x-some-extension"` + FoodBrands []string `json:"food_brands" extensions:"x-some-extension"` + SingleEnumVarname string `json:"single_enum_varname" swaggertype:"integer" enums:"1,2,3" x-enum-varnames:"one,two,three" extensions:"x-some-extension"` +} + +type Tag struct { + ID int `json:"id" format:"int64"` + Name string `json:"name"` + Pets []Pet `json:"pets"` +} + +type Tags []*Tag + +type AnonymousStructArray []struct { + Foo string `json:"foo"` +} + +type CrossAlias cross.Cross + +type Pet2 struct { + ID int `json:"id"` + MiddleName *string `json:"middlename" extensions:"x-nullable,x-abc=def,!x-omitempty"` + DeletedAt *time.Time `json:"deleted_at"` +} + +type IndirectRecursiveTest struct { + Tags []Tag +} + +type APIError struct { + ErrorCode int + ErrorMessage string + CreatedAt time.Time +} + +type RevValueBase struct { + Status bool `json:"Status"` + + Err int32 `json:"Err,omitempty"` +} +type RevValue struct { + RevValueBase `json:"rev_value_base"` + + Data int `json:"Data"` + Cross cross.Cross `json:"cross"` + Crosses []cross.Cross `json:"crosses"` +} + +// Below we have Pet5b as base type and Pet5a and Pet5c both have Pet5b as anonymous field, inheriting it's properties +// By using these names we ensure that our test will fill if the order of parsing matters at all + +type Pet5a struct { + *Pet5b + Odd bool `json:"odd" binding:"required"` +} + +type Pet5b struct { + Name string `json:"name" binding:"required"` +} + +type Pet5c struct { + *Pet5b + Odd bool `json:"odd" binding:"required"` +} diff --git a/typesv3.go b/typesv3.go new file mode 100644 index 000000000..a7143bcc1 --- /dev/null +++ b/typesv3.go @@ -0,0 +1,10 @@ +package swag + +import "github.com/sv-tools/openapi/spec" + +// SchemaV3 parsed schema. +type SchemaV3 struct { + *spec.Schema // + PkgPath string // package import path used to rename Name of a definition int case of conflict + Name string // Name in definitions +} diff --git a/version.go b/version.go index 7b928b229..d91efdbb7 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package swag // Version of swag. -const Version = "v1.8.12" +const Version = "v2.0.0" From 601fee1a7157d2e20c8bf54dfa83efd49257308c Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Wed, 5 Apr 2023 22:05:54 +0300 Subject: [PATCH 06/34] chore: add v2 to ci (#1532) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e129e778..d16a00809 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: build on: push: - branches: [ master ] + branches: [ master, v2 ] pull_request: - branches: [ master ] + branches: [ master, v2 ] jobs: test: From 72d07cd1669bc1ec0f65a77d5a49e695a03ef501 Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Wed, 5 Apr 2023 22:28:30 +0300 Subject: [PATCH 07/34] chore: linting code (#1533) * chore: make fmt * chore: make lint --- field_parser.go | 1 + field_parserv3.go | 12 ++++++------ operation.go | 6 +++--- operationv3.go | 4 ++-- parserv3.go | 32 ++++++++++++++++---------------- 5 files changed, 28 insertions(+), 27 deletions(-) diff --git a/field_parser.go b/field_parser.go index e317ea9b6..1716e5034 100644 --- a/field_parser.go +++ b/field_parser.go @@ -21,6 +21,7 @@ const ( swaggerTypeTag = "swaggertype" swaggerIgnoreTag = "swaggerignore" ) + var _ FieldParser = &tagBaseFieldParser{} type tagBaseFieldParser struct { diff --git a/field_parserv3.go b/field_parserv3.go index 068067be8..b276a462e 100644 --- a/field_parserv3.go +++ b/field_parserv3.go @@ -457,13 +457,13 @@ func (sf *structFieldV3) parseValidTags(validTag string) { } } -func (field *structFieldV3) parseEnumTags(enumTag string) error { - enumType := field.schemaType - if field.schemaType == ARRAY { - enumType = field.arrayType +func (sf *structFieldV3) parseEnumTags(enumTag string) error { + enumType := sf.schemaType + if sf.schemaType == ARRAY { + enumType = sf.arrayType } - field.enums = nil + sf.enums = nil for _, e := range strings.Split(enumTag, ",") { value, err := defineType(enumType, e) @@ -471,7 +471,7 @@ func (field *structFieldV3) parseEnumTags(enumTag string) error { return err } - field.enums = append(field.enums, value) + sf.enums = append(sf.enums, value) } return nil diff --git a/operation.go b/operation.go index 97523714f..308dc30ff 100644 --- a/operation.go +++ b/operation.go @@ -1215,9 +1215,9 @@ func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, bool, fileName := entry.Name() - isJson := strings.Contains(fileName, ".json") + isJSON := strings.Contains(fileName, ".json") isYaml := strings.Contains(fileName, ".yaml") - if !isJson && !isYaml { + if !isJSON && !isYaml { continue } @@ -1229,7 +1229,7 @@ func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, bool, return nil, false, fmt.Errorf("Failed to read code example file %s error: %s ", fullPath, err) } - return commentInfo, isJson, nil + return commentInfo, isJSON, nil } } diff --git a/operationv3.go b/operationv3.go index 219171634..e7575fba5 100644 --- a/operationv3.go +++ b/operationv3.go @@ -13,7 +13,7 @@ import ( "gopkg.in/yaml.v2" ) -// Operation describes a single API operation on a path. +// OperationV3 describes a single API operation on a path. // For more information: https://github.com/swaggo/swag#api-operation type OperationV3 struct { parser *Parser @@ -39,7 +39,7 @@ func NewOperationV3(parser *Parser, options ...func(*OperationV3)) *OperationV3 return operation } -// SetCodeExampleFilesDirectory sets the directory to search for codeExamples. +// SetCodeExampleFilesDirectoryV3 sets the directory to search for codeExamples. func SetCodeExampleFilesDirectoryV3(directoryPath string) func(*OperationV3) { return func(o *OperationV3) { o.codeExampleFilesDir = directoryPath diff --git a/parserv3.go b/parserv3.go index b0f49c5df..41a779135 100644 --- a/parserv3.go +++ b/parserv3.go @@ -14,10 +14,10 @@ import ( "github.com/sv-tools/openapi/spec" ) -// FieldParserFactory create FieldParser. +// FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 create FieldParser. type FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 -// FieldParser parse struct field. +// FieldParserV3 parse struct field. type FieldParserV3 interface { ShouldSkip() bool FieldName() (string, error) @@ -28,8 +28,8 @@ type FieldParserV3 interface { } // GetOpenAPI returns *spec.OpenAPI which is the root document object for the API specification. -func (parser *Parser) GetOpenAPI() *spec.OpenAPI { - return parser.openAPI +func (p *Parser) GetOpenAPI() *spec.OpenAPI { + return p.openAPI } func (p *Parser) parseGeneralAPIInfoV3(comments []string) error { @@ -419,8 +419,8 @@ func getSecurityDefinitionKey(lines []string) string { return "" } -// ParseRouterAPIInfo parses router api info for given astFile. -func (parser *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { +// ParseRouterAPIInfoV3 parses router api info for given astFile. +func (p *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { for _, astDescription := range fileInfo.File.Decls { if (fileInfo.ParseFlag & ParseOperations) == ParseNone { continue @@ -431,10 +431,10 @@ func (parser *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { continue } - if parser.matchTags(astDeclaration.Doc.List) && - matchExtension(parser.parseExtension, astDeclaration.Doc.List) { + if p.matchTags(astDeclaration.Doc.List) && + matchExtension(p.parseExtension, astDeclaration.Doc.List) { // for per 'function' comment, create a new 'Operation' object - operation := NewOperationV3(parser, SetCodeExampleFilesDirectoryV3(parser.codeExampleFilesDir)) + operation := NewOperationV3(p, SetCodeExampleFilesDirectoryV3(p.codeExampleFilesDir)) for _, comment := range astDeclaration.Doc.List { err := operation.ParseComment(comment.Text, fileInfo.File) @@ -442,7 +442,7 @@ func (parser *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { return fmt.Errorf("ParseComment error in file %s :%+v", fileInfo.Path, err) } } - err := processRouterOperationV3(parser, operation) + err := processRouterOperationV3(p, operation) if err != nil { return err } @@ -941,8 +941,8 @@ func (p *Parser) getRefTypeSchemaV3(typeSpecDef *TypeSpecDef, schema *SchemaV3) return refSchema } -// GetSchemaTypePath get path of schema type. -func (parser *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], depth int) []string { +// GetSchemaTypePathV3 get path of schema type. +func (p *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], depth int) []string { if schema == nil || depth == 0 { return nil } @@ -955,8 +955,8 @@ func (parser *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], d if name != "" { if pos := strings.LastIndexByte(name, '/'); pos >= 0 { name = name[pos+1:] - if schema, ok := parser.openAPI.Components.Spec.Schemas[name]; ok { - return parser.GetSchemaTypePathV3(schema, depth) + if schema, ok := p.openAPI.Components.Spec.Schemas[name]; ok { + return p.GetSchemaTypePathV3(schema, depth) } } @@ -970,7 +970,7 @@ func (parser *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], d s := []string{schema.Spec.Type[0]} - return append(s, parser.GetSchemaTypePathV3(schema.Spec.Items.Schema, depth)...) + return append(s, p.GetSchemaTypePathV3(schema.Spec.Items.Schema, depth)...) case OBJECT: if schema.Spec.AdditionalProperties != nil && schema.Spec.AdditionalProperties.Schema != nil { // for map @@ -978,7 +978,7 @@ func (parser *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], d s := []string{schema.Spec.Type[0]} - return append(s, parser.GetSchemaTypePathV3(schema.Spec.AdditionalProperties.Schema, depth)...) + return append(s, p.GetSchemaTypePathV3(schema.Spec.AdditionalProperties.Schema, depth)...) } } From 27719cd54ac8244f54213ffcff801352bf854451 Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Wed, 5 Apr 2023 23:32:18 +0300 Subject: [PATCH 08/34] chore: remove GOPATH support and go1.17 (#1536) --- .github/workflows/ci.yml | 18 ----------- README.md | 2 +- README_zh-CN.md | 2 +- generics.go | 3 -- generics_other.go | 42 ------------------------- generics_other_test.go | 67 ---------------------------------------- generics_test.go | 3 -- 7 files changed, 2 insertions(+), 135 deletions(-) delete mode 100644 generics_other.go delete mode 100644 generics_other_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d16a00809..7dad15e83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,36 +15,18 @@ jobs: runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v3 - with: - path: ./src/github.com/${{ github.repository }} - name: Set up Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: deps - working-directory: ./src/github.com/${{ github.repository }} run: make deps - env: - GOPATH: ${{ runner.workspace }} - name: static program analysis - working-directory: ./src/github.com/${{ github.repository }} run: | - export PATH=$PATH:$(go env GOPATH)/bin # https://github.com/actions/setup-go/issues/14 - mkdir -p $(go env GOPATH)/src/github.com/swaggo - ln -s $(pwd) $(go env GOPATH)/src/github.com/swaggo/swag make fmt-check lint vet - env: - GOPATH: ${{ runner.workspace }} - name: build run: make build - working-directory: ./src/github.com/${{ github.repository }} - env: - GOPATH: ${{ runner.workspace }} - name: test - working-directory: ./src/github.com/${{ github.repository }} run: make test - env: - GOPATH: ${{ runner.workspace }} - name: coverage - working-directory: ./src/github.com/${{ github.repository }} run: bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 325d9bd35..fb931fbbb 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a varie ```sh go install github.com/swaggo/swag/cmd/swag@latest ``` -To build from source you need [Go](https://golang.org/dl/) (1.17 or newer). +To build from source you need [Go](https://golang.org/dl/) (1.18 or newer). Or download a pre-compiled binary from the [release page](https://github.com/swaggo/swag/releases). diff --git a/README_zh-CN.md b/README_zh-CN.md index 60b6fb559..3ff52d0be 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -50,7 +50,7 @@ Swag将Go的注释转换为Swagger2.0文档。我们为流行的 [Go Web Framewo go install github.com/swaggo/swag/cmd/swag@latest ``` -从源码开始构建的话,需要有Go环境(1.17及以上版本)。 +从源码开始构建的话,需要有Go环境(1.18及以上版本)。 或者从github的release页面下载预编译好的二进制文件。 diff --git a/generics.go b/generics.go index bb6a09961..5f46b4d45 100644 --- a/generics.go +++ b/generics.go @@ -1,6 +1,3 @@ -//go:build go1.18 -// +build go1.18 - package swag import ( diff --git a/generics_other.go b/generics_other.go deleted file mode 100644 index 5fd9e8231..000000000 --- a/generics_other.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build !go1.18 -// +build !go1.18 - -package swag - -import ( - "fmt" - "github.com/go-openapi/spec" - "go/ast" -) - -type genericTypeSpec struct { - ArrayDepth int - TypeSpec *TypeSpecDef - Name string -} - -func (pkgDefs *PackagesDefinitions) parametrizeGenericType(file *ast.File, original *TypeSpecDef, fullGenericForm string) *TypeSpecDef { - return original -} - -func getGenericFieldType(file *ast.File, field ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) (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) { - // 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: - 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_test.go b/generics_other_test.go deleted file mode 100644 index 1a396a04b..000000000 --- a/generics_other_test.go +++ /dev/null @@ -1,67 +0,0 @@ -//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.parametrizeGenericType(&ast.File{}, tSpec, "") - assert.Equal(t, tr, tSpec) - - tr = pd.parametrizeGenericType(&ast.File{}, tSpec, "") - 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 8fa7bdfc3..bd79f93de 100644 --- a/generics_test.go +++ b/generics_test.go @@ -1,6 +1,3 @@ -//go:build go1.18 -// +build go1.18 - package swag import ( From 7394a48acdd8f855c6d7b9c7c7637793168a9590 Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Thu, 6 Apr 2023 00:26:16 +0300 Subject: [PATCH 09/34] chore: replace github.com/ghodss/yaml with sigs.k8s.io/yaml (#1520) (#1538) --- gen/gen.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gen/gen.go b/gen/gen.go index 5950ae345..04b2aeb04 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -17,10 +17,10 @@ import ( jsoniter "github.com/json-iterator/go" - "github.com/ghodss/yaml" "github.com/go-openapi/spec" openapi "github.com/sv-tools/openapi/spec" "github.com/swaggo/swag" + "sigs.k8s.io/yaml" ) var open = os.Open diff --git a/go.mod b/go.mod index 4bbb55185..5a6271167 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ go 1.18 require ( github.com/KyleBanks/depth v1.2.1 - github.com/ghodss/yaml v1.0.0 github.com/go-openapi/spec v0.20.8 github.com/json-iterator/go v1.1.12 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.2 github.com/sv-tools/openapi v0.2.1 golang.org/x/tools v0.7.0 + sigs.k8s.io/yaml v1.3.0 ) require ( diff --git a/go.sum b/go.sum index 177001055..3c097fdc8 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= @@ -83,3 +81,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= From 677b4c282649cbe23d94fab756c9bae2a1eba5a3 Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Thu, 6 Apr 2023 02:36:27 +0300 Subject: [PATCH 10/34] chore: refactor code (#1539) * chore: refactor code * chore: lint * chore: lint * chore: use embed for generated doc --- cmd/swag/main.go | 2 +- gen/gen.go | 209 +++++++++++++++++++++++++------------------- gen/gen_test.go | 41 --------- gen/genv3.go | 202 ------------------------------------------ gen/src/oas2.tmpl | 23 +++++ gen/src/oas3.tmpl | 20 +++++ operationv3_test.go | 9 +- parser.go | 6 +- parserv3_test.go | 30 +++---- utils.go | 28 +++++- utils_go18.go | 31 ------- utils_other.go | 47 ---------- 12 files changed, 215 insertions(+), 433 deletions(-) delete mode 100644 gen/genv3.go create mode 100644 gen/src/oas2.tmpl create mode 100644 gen/src/oas3.tmpl delete mode 100644 utils_go18.go delete mode 100644 utils_other.go diff --git a/cmd/swag/main.go b/cmd/swag/main.go index f2164e10d..f9f12e710 100644 --- a/cmd/swag/main.go +++ b/cmd/swag/main.go @@ -207,7 +207,7 @@ func initAction(ctx *cli.Context) error { Tags: ctx.String(tagsFlag), PackageName: ctx.String(packageName), Debugger: logger, - OpenAPIVersion: ctx.Bool(openAPIVersionFlag), + GenerateOpenAPI3Doc: ctx.Bool(openAPIVersionFlag), CollectionFormat: collectionFormat, }) } diff --git a/gen/gen.go b/gen/gen.go index 04b2aeb04..1baea3b2a 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -3,6 +3,7 @@ package gen import ( "bufio" "bytes" + "embed" "encoding/json" "fmt" "go/format" @@ -17,8 +18,9 @@ import ( jsoniter "github.com/json-iterator/go" - "github.com/go-openapi/spec" - openapi "github.com/sv-tools/openapi/spec" + v2 "github.com/go-openapi/spec" + v3 "github.com/sv-tools/openapi/spec" + "github.com/swaggo/swag" "sigs.k8s.io/yaml" ) @@ -28,18 +30,20 @@ var open = os.Open // DefaultOverridesFile is the location swagger will look for type overrides. const DefaultOverridesFile = ".swaggo" -type genTypeWriter func(*Config, *spec.Swagger) error +type genTypeWriter func(*Config, interface{}) error // Gen presents a generate tool for swag. type Gen struct { - json func(data interface{}) ([]byte, error) - jsonIndent func(data interface{}) ([]byte, error) - jsonToYAML func(data []byte) ([]byte, error) - outputTypeMap map[string]genTypeWriter - outputTypeMapV3 map[string]openAPITypeWriter - debug Debugger + json func(data interface{}) ([]byte, error) + jsonIndent func(data interface{}) ([]byte, error) + jsonToYAML func(data []byte) ([]byte, error) + outputTypeMap map[string]genTypeWriter + debug Debugger } +//go:embed src/*.tmpl +var tmpl embed.FS + // Debugger is the interface that wraps the basic Printf method. type Debugger interface { Printf(format string, v ...interface{}) @@ -50,25 +54,17 @@ func New() *Gen { gen := Gen{ json: json.Marshal, jsonIndent: func(data interface{}) ([]byte, error) { - var json = jsoniter.ConfigCompatibleWithStandardLibrary - return json.MarshalIndent(&data, "", " ") + return jsoniter.ConfigCompatibleWithStandardLibrary.MarshalIndent(&data, "", " ") }, jsonToYAML: yaml.JSONToYAML, debug: log.New(os.Stdout, "", log.LstdFlags), } gen.outputTypeMap = map[string]genTypeWriter{ - "go": gen.writeDocSwagger, - "json": gen.writeJSONSwagger, - "yaml": gen.writeYAMLSwagger, - "yml": gen.writeYAMLSwagger, - } - - gen.outputTypeMapV3 = map[string]openAPITypeWriter{ - "go": gen.writeDocOpenAPI, - "json": gen.writeJSONOpenAPI, - "yaml": gen.writeYAMLOpenAPI, - "yml": gen.writeYAMLOpenAPI, + "go": gen.writeDoc, + "json": gen.writeJSON, + "yaml": gen.writeYAML, + "yml": gen.writeYAML, } return &gen @@ -139,8 +135,9 @@ type Config struct { // include only tags mentioned when searching, comma separated Tags string - // if true, OpenAPI V3.1 spec will be generated - OpenAPIVersion bool + // GenerateOpenAPI3Doc if true, OpenAPI V3.1 spec will be generated + GenerateOpenAPI3Doc bool + // PackageName defines package name of generated `docs.go` PackageName string @@ -196,7 +193,7 @@ func (g *Gen) Build(config *Config) error { swag.SetOverrides(overrides), swag.ParseUsingGoList(config.ParseGoList), swag.SetTags(config.Tags), - swag.SetOpenAPIVersion(config.OpenAPIVersion), + swag.GenerateOpenAPI3Doc(config.GenerateOpenAPI3Doc), swag.SetCollectionFormat(config.CollectionFormat), ) @@ -213,45 +210,18 @@ func (g *Gen) Build(config *Config) error { return err } - if config.OpenAPIVersion { - openAPI := p.GetOpenAPI() - err := g.writeOpenAPI(config, openAPI) - if err != nil { - return err - } - - return nil - } - - swagger := p.GetSwagger() - err := g.writeSwagger(config, swagger) - if err != nil { - return err + if config.GenerateOpenAPI3Doc { + return g.writeOpenAPI(config, p.GetOpenAPI()) } - return nil + return g.writeOpenAPI(config, p.GetSwagger()) } -func (g *Gen) writeOpenAPI(config *Config, o *openapi.OpenAPI) error { - for _, outputType := range config.OutputTypes { - outputType = strings.ToLower(strings.TrimSpace(outputType)) - if typeWriter, ok := g.outputTypeMapV3[outputType]; ok { - if err := typeWriter(config, o); err != nil { - return err - } - } else { - log.Printf("output type '%s' not supported", outputType) - } - } - - return nil -} - -func (g *Gen) writeSwagger(config *Config, swagger *spec.Swagger) error { +func (g *Gen) writeOpenAPI(config *Config, doc interface{}) error { for _, outputType := range config.OutputTypes { outputType = strings.ToLower(strings.TrimSpace(outputType)) if typeWriter, ok := g.outputTypeMap[outputType]; ok { - if err := typeWriter(config, swagger); err != nil { + if err := typeWriter(config, doc); err != nil { return err } } else { @@ -262,7 +232,7 @@ func (g *Gen) writeSwagger(config *Config, swagger *spec.Swagger) error { return nil } -func (g *Gen) writeDocSwagger(config *Config, swagger *spec.Swagger) error { +func (g *Gen) writeDoc(config *Config, doc interface{}) error { var filename = "docs.go" if config.InstanceName != swag.Name { @@ -291,17 +261,25 @@ func (g *Gen) writeDocSwagger(config *Config, swagger *spec.Swagger) error { defer docs.Close() // Write doc - err = g.writeGoDoc(packageName, docs, swagger, config) - if err != nil { - return err - } + switch spec := doc.(type) { + case *v2.Swagger: + err = g.writeGoDoc(packageName, docs, spec, config) + if err != nil { + return err + } + case *v3.OpenAPI: + err = g.writeGoDocV3(packageName, docs, spec, config) + if err != nil { + return nil + } + } g.debug.Printf("create docs.go at %+v", docFileName) return nil } -func (g *Gen) writeJSONSwagger(config *Config, swagger *spec.Swagger) error { +func (g *Gen) writeJSON(config *Config, spec interface{}) error { var filename = "swagger.json" if config.InstanceName != swag.Name { @@ -310,7 +288,7 @@ func (g *Gen) writeJSONSwagger(config *Config, swagger *spec.Swagger) error { jsonFileName := path.Join(config.OutputDir, filename) - b, err := g.jsonIndent(swagger) + b, err := g.jsonIndent(spec) if err != nil { return err } @@ -325,7 +303,7 @@ func (g *Gen) writeJSONSwagger(config *Config, swagger *spec.Swagger) error { return nil } -func (g *Gen) writeYAMLSwagger(config *Config, swagger *spec.Swagger) error { +func (g *Gen) writeYAML(config *Config, swagger interface{}) error { var filename = "swagger.yaml" if config.InstanceName != swag.Name { @@ -421,29 +399,29 @@ func parseOverrides(r io.Reader) (map[string]string, error) { return overrides, nil } -func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swagger, config *Config) error { - generator, err := template.New("swagger_info").Funcs(template.FuncMap{ +func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *v2.Swagger, config *Config) error { + generator, err := template.New("oas2.tmpl").Funcs(template.FuncMap{ "printDoc": func(v string) string { // Add schemes v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:] // Sanitize backticks return strings.Replace(v, "`", "`+\"`\"+`", -1) }, - }).Parse(packageTemplate) + }).ParseFS(tmpl, "src/*.tmpl") if err != nil { return err } - swaggerSpec := &spec.Swagger{ + swaggerSpec := &v2.Swagger{ VendorExtensible: swagger.VendorExtensible, - SwaggerProps: spec.SwaggerProps{ + SwaggerProps: v2.SwaggerProps{ ID: swagger.ID, Consumes: swagger.Consumes, Produces: swagger.Produces, Swagger: swagger.Swagger, - Info: &spec.Info{ + Info: &v2.Info{ VendorExtensible: swagger.Info.VendorExtensible, - InfoProps: spec.InfoProps{ + InfoProps: v2.InfoProps{ Description: "{{escape .Description}}", Title: "{{.Title}}", TermsOfService: swagger.Info.TermsOfService, @@ -510,27 +488,78 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *spec.Swa return err } -var packageTemplate = `// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT. +func (g *Gen) writeGoDocV3(packageName string, output io.Writer, openAPI *v3.OpenAPI, config *Config) error { + generator, err := template.New("oas3.tmpl").Funcs(template.FuncMap{ + "printDoc": func(v string) string { + // Add schemes + v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:] + // Sanitize backticks + return strings.Replace(v, "`", "`+\"`\"+`", -1) + }, + }).ParseFS(tmpl, "src/*.tmpl") + if err != nil { + return err + } + + openAPISpec := v3.OpenAPI{ + Components: openAPI.Components, + OpenAPI: openAPI.OpenAPI, + Info: &v3.Extendable[v3.Info]{ + Spec: &v3.Info{ + Description: "{{escape .Description}}", + Title: "{{.Title}}", + Version: "{{.Version}}", + TermsOfService: openAPI.Info.Spec.TermsOfService, + Contact: openAPI.Info.Spec.Contact, + License: openAPI.Info.Spec.License, + Summary: openAPI.Info.Spec.Summary, + }, + Extensions: openAPI.Info.Extensions, + }, + ExternalDocs: openAPI.ExternalDocs, + Paths: openAPI.Paths, + WebHooks: openAPI.WebHooks, + JsonSchemaDialect: openAPI.JsonSchemaDialect, + Security: openAPI.Security, + Tags: openAPI.Tags, + Servers: openAPI.Servers, + } -package docs + // crafted docs.json + buf, err := g.jsonIndent(openAPISpec) + if err != nil { + return err + } -import "github.com/swaggo/swag" + buffer := &bytes.Buffer{} -const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = ` + "`{{ printDoc .Doc}}`" + ` + err = generator.Execute(buffer, struct { + Timestamp time.Time + Doc string + PackageName string + Title string + Description string + Version string + InstanceName string + GeneratedTime bool + }{ + Timestamp: time.Now(), + GeneratedTime: config.GeneratedTime, + Doc: string(buf), + PackageName: packageName, + Title: openAPI.Info.Spec.Title, + Description: openAPI.Info.Spec.Description, + Version: openAPI.Info.Spec.Version, + InstanceName: config.InstanceName, + }) + if err != nil { + return err + } -// SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} holds exported Swagger Info so clients can modify it -var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = &swag.Spec{ - Version: {{ printf "%q" .Version}}, - Host: {{ printf "%q" .Host}}, - BasePath: {{ printf "%q" .BasePath}}, - Schemes: []string{ {{ range $index, $schema := .Schemes}}{{if gt $index 0}},{{end}}{{printf "%q" $schema}}{{end}} }, - Title: {{ printf "%q" .Title}}, - Description: {{ printf "%q" .Description}}, - InfoInstanceName: {{ printf "%q" .InstanceName }}, - SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, -} + code := g.formatSource(buffer.Bytes()) -func init() { - swag.Register(SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}) + // write + _, err = output.Write(code) + + return err } -` diff --git a/gen/gen_test.go b/gen/gen_test.go index 62a313e6a..9169dd355 100644 --- a/gen/gen_test.go +++ b/gen/gen_test.go @@ -14,7 +14,6 @@ import ( "strings" "testing" - "github.com/go-openapi/spec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/swaggo/swag" @@ -516,46 +515,6 @@ func (w *mockWriter) Write(data []byte) (int, error) { return len(data), nil } -func TestGen_writeGoDoc(t *testing.T) { - gen := New() - - swapTemplate := packageTemplate - - packageTemplate = `{{{` - err := gen.writeGoDoc("docs", nil, nil, &Config{}) - assert.Error(t, err) - - packageTemplate = `{{.Data}}` - swagger := &spec.Swagger{ - VendorExtensible: spec.VendorExtensible{}, - SwaggerProps: spec.SwaggerProps{ - Info: &spec.Info{}, - }, - } - - err = gen.writeGoDoc("docs", &mockWriter{}, swagger, &Config{}) - assert.Error(t, err) - - packageTemplate = `{{ if .GeneratedTime }}Fake Time{{ end }}` - err = gen.writeGoDoc("docs", - &mockWriter{ - hook: func(data []byte) { - assert.Equal(t, "Fake Time", string(data)) - }, - }, swagger, &Config{GeneratedTime: true}) - assert.NoError(t, err) - - err = gen.writeGoDoc("docs", - &mockWriter{ - hook: func(data []byte) { - assert.Equal(t, "", string(data)) - }, - }, swagger, &Config{GeneratedTime: false}) - assert.NoError(t, err) - - packageTemplate = swapTemplate -} - func TestGen_GeneratedDoc(t *testing.T) { config := &Config{ SearchDir: searchDir, diff --git a/gen/genv3.go b/gen/genv3.go deleted file mode 100644 index 8010660f2..000000000 --- a/gen/genv3.go +++ /dev/null @@ -1,202 +0,0 @@ -package gen - -import ( - "bytes" - "fmt" - "io" - "os" - "path" - "path/filepath" - "strings" - "text/template" - "time" - - "github.com/sv-tools/openapi/spec" - "github.com/swaggo/swag" -) - -type openAPITypeWriter func(*Config, *spec.OpenAPI) error - -func (g *Gen) writeDocOpenAPI(config *Config, openAPI *spec.OpenAPI) error { - var filename = "docs.go" - - if config.InstanceName != swag.Name { - filename = config.InstanceName + "_" + filename - } - - docFileName := path.Join(config.OutputDir, filename) - - absOutputDir, err := filepath.Abs(config.OutputDir) - if err != nil { - return err - } - - packageName := filepath.Base(absOutputDir) - - docs, err := os.Create(docFileName) - if err != nil { - return err - } - defer docs.Close() - - // Write doc - err = g.writeGoDocV3(packageName, docs, openAPI, config) - if err != nil { - return err - } - - g.debug.Printf("create docs.go at %+v", docFileName) - - return nil -} - -func (g *Gen) writeJSONOpenAPI(config *Config, swagger *spec.OpenAPI) error { - var filename = "swagger.json" - - if config.InstanceName != swag.Name { - filename = config.InstanceName + "_" + filename - } - - jsonFileName := path.Join(config.OutputDir, filename) - - b, err := g.jsonIndent(swagger) - if err != nil { - return err - } - - err = g.writeFile(b, jsonFileName) - if err != nil { - return err - } - - g.debug.Printf("create swagger.json at %+v", jsonFileName) - - return nil -} - -func (g *Gen) writeYAMLOpenAPI(config *Config, swagger *spec.OpenAPI) error { - var filename = "swagger.yaml" - - if config.InstanceName != swag.Name { - filename = config.InstanceName + "_" + filename - } - - yamlFileName := path.Join(config.OutputDir, filename) - - b, err := g.json(swagger) - if err != nil { - return err - } - - y, err := g.jsonToYAML(b) - if err != nil { - return fmt.Errorf("cannot covert json to yaml error: %s", err) - } - - err = g.writeFile(y, yamlFileName) - if err != nil { - return err - } - - g.debug.Printf("create swagger.yaml at %+v", yamlFileName) - - return nil -} - -func (g *Gen) writeGoDocV3(packageName string, output io.Writer, openAPI *spec.OpenAPI, config *Config) error { - generator, err := template.New("swagger_info").Funcs(template.FuncMap{ - "printDoc": func(v string) string { - // Add schemes - v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:] - // Sanitize backticks - return strings.Replace(v, "`", "`+\"`\"+`", -1) - }, - }).Parse(packageTemplateV3) - if err != nil { - return err - } - - openAPISpec := spec.OpenAPI{ - Components: openAPI.Components, - OpenAPI: openAPI.OpenAPI, - Info: &spec.Extendable[spec.Info]{ - Spec: &spec.Info{ - Description: "{{escape .Description}}", - Title: "{{.Title}}", - Version: "{{.Version}}", - TermsOfService: openAPI.Info.Spec.TermsOfService, - Contact: openAPI.Info.Spec.Contact, - License: openAPI.Info.Spec.License, - Summary: openAPI.Info.Spec.Summary, - }, - Extensions: openAPI.Info.Extensions, - }, - ExternalDocs: openAPI.ExternalDocs, - Paths: openAPI.Paths, - WebHooks: openAPI.WebHooks, - JsonSchemaDialect: openAPI.JsonSchemaDialect, - Security: openAPI.Security, - Tags: openAPI.Tags, - Servers: openAPI.Servers, - } - - // crafted docs.json - buf, err := g.jsonIndent(openAPISpec) - if err != nil { - return err - } - - buffer := &bytes.Buffer{} - - err = generator.Execute(buffer, struct { - Timestamp time.Time - Doc string - PackageName string - Title string - Description string - Version string - InstanceName string - GeneratedTime bool - }{ - Timestamp: time.Now(), - GeneratedTime: config.GeneratedTime, - Doc: string(buf), - PackageName: packageName, - Title: openAPI.Info.Spec.Title, - Description: openAPI.Info.Spec.Description, - Version: openAPI.Info.Spec.Version, - InstanceName: config.InstanceName, - }) - if err != nil { - return err - } - - code := g.formatSource(buffer.Bytes()) - - // write - _, err = output.Write(code) - - return err -} - -var packageTemplateV3 = `// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT - -package docs - -import "github.com/swaggo/swag" - -const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = ` + "`{{ printDoc .Doc}}`" + ` - -// SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} holds exported Swagger Info so clients can modify it -var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = &swag.Spec{ - Version: {{ printf "%q" .Version}}, - Title: {{ printf "%q" .Title}}, - Description: {{ printf "%q" .Description}}, - InfoInstanceName: {{ printf "%q" .InstanceName }}, - SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, -} - -func init() { - swag.Register(SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}) -} -` diff --git a/gen/src/oas2.tmpl b/gen/src/oas2.tmpl new file mode 100644 index 000000000..b7bce69b9 --- /dev/null +++ b/gen/src/oas2.tmpl @@ -0,0 +1,23 @@ +// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT. + +package docs + +import "github.com/swaggo/swag" + +const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = `{{ printDoc .Doc}}` + +// SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} holds exported Swagger Info so clients can modify it +var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = &swag.Spec{ + Version: {{ printf "%q" .Version}}, + Host: {{ printf "%q" .Host}}, + BasePath: {{ printf "%q" .BasePath}}, + Schemes: []string{ {{ range $index, $schema := .Schemes}}{{if gt $index 0}},{{end}}{{printf "%q" $schema}}{{end}} }, + Title: {{ printf "%q" .Title}}, + Description: {{ printf "%q" .Description}}, + InfoInstanceName: {{ printf "%q" .InstanceName }}, + SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, +} + +func init() { + swag.Register(SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}) +} diff --git a/gen/src/oas3.tmpl b/gen/src/oas3.tmpl new file mode 100644 index 000000000..20cd33af8 --- /dev/null +++ b/gen/src/oas3.tmpl @@ -0,0 +1,20 @@ +// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT + +package docs + +import "github.com/swaggo/swag" + +const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = `{{ printDoc .Doc}}` + +// SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} holds exported Swagger Info so clients can modify it +var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = &swag.Spec{ + Version: {{ printf "%q" .Version}}, + Title: {{ printf "%q" .Title}}, + Description: {{ printf "%q" .Description}}, + InfoInstanceName: {{ printf "%q" .InstanceName }}, + SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, +} + +func init() { + swag.Register(SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}) +} \ No newline at end of file diff --git a/operationv3_test.go b/operationv3_test.go index 5b552caca..5b96ab043 100644 --- a/operationv3_test.go +++ b/operationv3_test.go @@ -234,8 +234,13 @@ func TestParseResponseCommentWithNestedPrimitiveTypeV3(t *testing.T) { allOf := operation.Responses.Spec.Response["200"].Spec.Spec.Content["application/json"].Spec.Schema.Spec.AllOf require.NotNil(t, allOf) assert.Equal(t, 2, len(allOf)) - assert.Equal(t, "#/components/schemas/data", allOf[0].Ref.Ref) - assert.Equal(t, "#/components/schemas/data2", allOf[1].Ref.Ref) + found := map[string]struct{}{} + for _, schema := range allOf { + assert.NotNil(t, schema.Ref.Ref) + found[schema.Ref.Ref] = struct{}{} + } + assert.NotNil(t, found["#/components/schemas/data"]) + assert.NotNil(t, found["#/components/schemas/data2"]) } func TestParseResponseCommentWithNestedPrimitiveArrayTypeV3(t *testing.T) { diff --git a/parser.go b/parser.go index 916a8620c..0f92110eb 100644 --- a/parser.go +++ b/parser.go @@ -370,10 +370,10 @@ func ParseUsingGoList(enabled bool) func(parser *Parser) { } } -// SetOpenAPIVersion parses only those operations which match given extension -func SetOpenAPIVersion(openAPIVersion bool) func(*Parser) { +// GenerateOpenAPI3Doc parses only those operations which match given extension +func GenerateOpenAPI3Doc(enable bool) func(*Parser) { return func(p *Parser) { - p.openAPIVersion = openAPIVersion + p.openAPIVersion = enable } } diff --git a/parserv3_test.go b/parserv3_test.go index 990b656d4..70680722f 100644 --- a/parserv3_test.go +++ b/parserv3_test.go @@ -108,7 +108,7 @@ func TestParserParseGeneralApiInfoV3(t *testing.T) { gopath := os.Getenv("GOPATH") assert.NotNil(t, gopath) - p := New(SetOpenAPIVersion(true)) + p := New(GenerateOpenAPI3Doc(true)) err := p.ParseGeneralAPIInfo("testdata/v3/main.go") assert.NoError(t, err) @@ -172,7 +172,7 @@ func TestParser_ParseGeneralApiInfoExtensionsV3(t *testing.T) { gopath := os.Getenv("GOPATH") assert.NotNil(t, gopath) - p := New(SetOpenAPIVersion(true)) + p := New(GenerateOpenAPI3Doc(true)) err := p.ParseGeneralAPIInfo("testdata/v3/extensionsFail1.go") if assert.Error(t, err) { @@ -188,7 +188,7 @@ func TestParser_ParseGeneralApiInfoExtensionsV3(t *testing.T) { gopath := os.Getenv("GOPATH") assert.NotNil(t, gopath) - p := New(SetOpenAPIVersion(true)) + p := New(GenerateOpenAPI3Doc(true)) err := p.ParseGeneralAPIInfo("testdata/v3/extensionsFail2.go") if assert.Error(t, err) { @@ -203,7 +203,7 @@ func TestParserParseGeneralApiInfoWithOpsInSameFileV3(t *testing.T) { gopath := os.Getenv("GOPATH") assert.NotNil(t, gopath) - p := New(SetOpenAPIVersion(true)) + p := New(GenerateOpenAPI3Doc(true)) err := p.ParseGeneralAPIInfo("testdata/single_file_api/main.go") assert.NoError(t, err) @@ -216,7 +216,7 @@ func TestParserParseGeneralApiInfoWithOpsInSameFileV3(t *testing.T) { func TestParserParseGeneralAPIInfoMarkdownV3(t *testing.T) { t.Parallel() - p := New(SetMarkdownFileDirectory("testdata"), SetOpenAPIVersion(true)) + p := New(SetMarkdownFileDirectory("testdata"), GenerateOpenAPI3Doc(true)) mainAPIFile := "testdata/markdown.go" err := p.ParseGeneralAPIInfo(mainAPIFile) assert.NoError(t, err) @@ -224,7 +224,7 @@ func TestParserParseGeneralAPIInfoMarkdownV3(t *testing.T) { assert.Equal(t, "users", p.openAPI.Tags[0].Spec.Name) assert.Equal(t, "Users Tag Markdown Description", p.openAPI.Tags[0].Spec.Description) - p = New(SetOpenAPIVersion(true)) + p = New(GenerateOpenAPI3Doc(true)) err = p.ParseGeneralAPIInfo(mainAPIFile) assert.Error(t, err) @@ -235,14 +235,14 @@ func TestParserParseGeneralApiInfoFailedV3(t *testing.T) { gopath := os.Getenv("GOPATH") assert.NotNil(t, gopath) - p := New(SetOpenAPIVersion(true)) + p := New(GenerateOpenAPI3Doc(true)) assert.Error(t, p.ParseGeneralAPIInfo("testdata/noexist.go")) } func TestParserParseGeneralAPIInfoCollectionFormatV3(t *testing.T) { t.Parallel() - parser := New(SetOpenAPIVersion(true)) + parser := New(GenerateOpenAPI3Doc(true)) assert.NoError(t, parser.parseGeneralAPIInfoV3([]string{ "@query.collection.format csv", })) @@ -257,7 +257,7 @@ func TestParserParseGeneralAPIInfoCollectionFormatV3(t *testing.T) { func TestParserParseGeneralAPITagGroupsV3(t *testing.T) { t.Parallel() - parser := New(SetOpenAPIVersion(true)) + parser := New(GenerateOpenAPI3Doc(true)) assert.NoError(t, parser.parseGeneralAPIInfoV3([]string{ "@x-tagGroups [{\"name\":\"General\",\"tags\":[\"lanes\",\"video-recommendations\"]}]", })) @@ -269,12 +269,12 @@ func TestParserParseGeneralAPITagGroupsV3(t *testing.T) { func TestParserParseGeneralAPITagDocsV3(t *testing.T) { t.Parallel() - parser := New(SetOpenAPIVersion(true)) + parser := New(GenerateOpenAPI3Doc(true)) assert.Error(t, parser.parseGeneralAPIInfoV3([]string{ "@tag.name Test", "@tag.docs.description Best example documentation"})) - parser = New(SetOpenAPIVersion(true)) + parser = New(GenerateOpenAPI3Doc(true)) err := parser.parseGeneralAPIInfoV3([]string{ "@tag.name test", "@tag.description A test Tag", @@ -293,7 +293,7 @@ func TestGetAllGoFileInfoV3(t *testing.T) { searchDir := "testdata/pet" - p := New(SetOpenAPIVersion(true)) + p := New(GenerateOpenAPI3Doc(true)) err := p.getAllGoFileInfo("testdata", searchDir) assert.NoError(t, err) @@ -305,7 +305,7 @@ func TestParser_ParseTypeV3(t *testing.T) { searchDir := "testdata/v3/simple/" - p := New(SetOpenAPIVersion(true)) + p := New(GenerateOpenAPI3Doc(true)) err := p.getAllGoFileInfo("testdata", searchDir) assert.NoError(t, err) @@ -322,7 +322,7 @@ func TestParsePet(t *testing.T) { searchDir := "testdata/v3/pet" - p := New(SetOpenAPIVersion(true)) + p := New(GenerateOpenAPI3Doc(true)) p.PropNamingStrategy = PascalCase err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) @@ -348,7 +348,7 @@ func TestParseSimpleApiV3(t *testing.T) { t.Parallel() searchDir := "testdata/v3/simple" - p := New(SetOpenAPIVersion(true)) + p := New(GenerateOpenAPI3Doc(true)) p.PropNamingStrategy = PascalCase err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) diff --git a/utils.go b/utils.go index df31ff2e1..8272c4cc8 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,10 @@ package swag -import "unicode" +import ( + "reflect" + "unicode" + "unicode/utf8" +) // FieldsFunc split a string s by a func splitter into max n parts func FieldsFunc(s string, f func(rune2 rune) bool, n int) []string { @@ -53,3 +57,25 @@ func FieldsFunc(s string, f func(rune2 rune) bool, n int) []string { func FieldsByAnySpace(s string, n int) []string { return FieldsFunc(s, unicode.IsSpace, n) } + +// AppendUtf8Rune appends the UTF-8 encoding of r to the end of p and +// returns the extended buffer. If the rune is out of range, +// it appends the encoding of RuneError. +func AppendUtf8Rune(p []byte, r rune) []byte { + return utf8.AppendRune(p, r) +} + +// CanIntegerValue a wrapper of reflect.Value +type CanIntegerValue struct { + reflect.Value +} + +// CanInt reports whether Uint can be used without panicking. +func (v CanIntegerValue) CanInt() bool { + return v.Value.CanInt() +} + +// CanUint reports whether Uint can be used without panicking. +func (v CanIntegerValue) CanUint() bool { + return v.Value.CanUint() +} diff --git a/utils_go18.go b/utils_go18.go deleted file mode 100644 index 814f93433..000000000 --- a/utils_go18.go +++ /dev/null @@ -1,31 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package swag - -import ( - "reflect" - "unicode/utf8" -) - -// AppendUtf8Rune appends the UTF-8 encoding of r to the end of p and -// returns the extended buffer. If the rune is out of range, -// it appends the encoding of RuneError. -func AppendUtf8Rune(p []byte, r rune) []byte { - return utf8.AppendRune(p, r) -} - -// CanIntegerValue a wrapper of reflect.Value -type CanIntegerValue struct { - reflect.Value -} - -// CanInt reports whether Uint can be used without panicking. -func (v CanIntegerValue) CanInt() bool { - return v.Value.CanInt() -} - -// CanUint reports whether Uint can be used without panicking. -func (v CanIntegerValue) CanUint() bool { - return v.Value.CanUint() -} diff --git a/utils_other.go b/utils_other.go deleted file mode 100644 index 531c0df12..000000000 --- a/utils_other.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build !go1.18 -// +build !go1.18 - -package swag - -import ( - "reflect" - "unicode/utf8" -) - -// AppendUtf8Rune appends the UTF-8 encoding of r to the end of p and -// returns the extended buffer. If the rune is out of range, -// it appends the encoding of RuneError. -func AppendUtf8Rune(p []byte, r rune) []byte { - length := utf8.RuneLen(rune(r)) - if length > 0 { - utf8Slice := make([]byte, length) - utf8.EncodeRune(utf8Slice, rune(r)) - p = append(p, utf8Slice...) - } - return p -} - -// CanIntegerValue a wrapper of reflect.Value -type CanIntegerValue struct { - reflect.Value -} - -// CanInt reports whether Uint can be used without panicking. -func (v CanIntegerValue) CanInt() bool { - switch v.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return true - default: - return false - } -} - -// CanUint reports whether Uint can be used without panicking. -func (v CanIntegerValue) CanUint() bool { - switch v.Kind() { - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return true - default: - return false - } -} From 6e63ab7ab87a1642b4b48d3cfa84a0561f4cdf6e Mon Sep 17 00:00:00 2001 From: Nerzal Date: Sat, 8 Apr 2023 01:06:46 +0200 Subject: [PATCH 11/34] Support accept Header & Use RequestBody (#1541) --- operationv3.go | 186 +++++++++++++++++++++++++++++++------- operationv3_test.go | 214 ++++++++++++++++++++------------------------ 2 files changed, 250 insertions(+), 150 deletions(-) diff --git a/operationv3.go b/operationv3.go index e7575fba5..f4e80a497 100644 --- a/operationv3.go +++ b/operationv3.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/pkg/errors" "github.com/sv-tools/openapi/spec" "gopkg.in/yaml.v2" ) @@ -145,16 +146,49 @@ func (o *OperationV3) ParseTagsComment(commentLine string) { func (o *OperationV3) ParseAcceptComment(commentLine string) error { const errMessage = "could not parse accept comment" - // TODO this must be moved into another comment - // return parseMimeTypeList(commentLine, &o.RequestBody.Spec.Spec.Content, ) - // result, err := parseMimeTypeListV3(commentLine, "%v accept type can't be accepted") - // if err != nil { - // return errors.Wrap(err, errMessage) - // } + validTypes, err := parseMimeTypeListV3(commentLine, "%v accept type can't be accepted") + if err != nil { + return errors.Wrap(err, errMessage) + } - // for _, value := range result { - // o.RequestBody.Spec.Spec.Content[value] = spec.NewMediaType() - // } + if o.RequestBody == nil { + o.RequestBody = spec.NewRequestBodySpec() + } + + if o.RequestBody.Spec.Spec.Content == nil { + o.RequestBody.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType], len(validTypes)) + } + + for _, value := range validTypes { + // skip correctly setup types like application/json + if o.RequestBody.Spec.Spec.Content[value] != nil { + continue + } + + mediaType := spec.NewMediaType() + schema := spec.NewSchemaSpec() + + switch value { + case "application/json", "multipart/form-data", "text/xml": + schema.Spec.Type = spec.NewSingleOrArray(OBJECT) + case "image/png", + "image/jpeg", + "image/gif", + "application/octet-stream", + "application/pdf", + "application/msexcel", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": + schema.Spec.Type = spec.NewSingleOrArray(STRING) + schema.Spec.Format = "binary" + default: + schema.Spec.Type = spec.NewSingleOrArray(STRING) + } + + mediaType.Spec.Schema = schema + o.RequestBody.Spec.Spec.Content[value] = mediaType + } return nil } @@ -316,15 +350,33 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e } case "body": if objectType == PRIMITIVE { - param.Schema = PrimitiveSchemaV3(refType) - } else { - schema, err := o.parseAPIObjectSchema(commentLine, objectType, refType, astFile) + schema := PrimitiveSchemaV3(refType) + + err := o.parseParamAttributeForBody(commentLine, objectType, refType, schema.Spec) if err != nil { return err } - param.Schema = schema + o.fillRequestBody(schema, required, description, true) + + return nil + + } + + schema, err := o.parseAPIObjectSchema(commentLine, objectType, refType, astFile) + if err != nil { + return err } + + err = o.parseParamAttributeForBody(commentLine, objectType, refType, schema.Spec) + if err != nil { + return err + } + + o.fillRequestBody(schema, required, description, false) + + return nil + default: return fmt.Errorf("%s is not supported paramType", paramType) } @@ -343,7 +395,74 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e return nil } +func (o *OperationV3) fillRequestBody(schema *spec.RefOrSpec[spec.Schema], required bool, description string, primitive bool) { + if o.RequestBody == nil { + o.RequestBody = spec.NewRequestBodySpec() + o.RequestBody.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) + + if primitive { + o.RequestBody.Spec.Spec.Content["text/plain"] = spec.NewMediaType() + } else { + o.RequestBody.Spec.Spec.Content["application/json"] = spec.NewMediaType() + } + } + + o.RequestBody.Spec.Spec.Description = description + o.RequestBody.Spec.Spec.Required = required + + for _, value := range o.RequestBody.Spec.Spec.Content { + value.Spec.Schema = schema + } +} + func (o *OperationV3) parseParamAttribute(comment, objectType, schemaType string, param *spec.Parameter) error { + if param == nil { + return fmt.Errorf("cannot parse empty parameter for comment: %s", comment) + } + + schemaType = TransToValidSchemeType(schemaType) + + for attrKey, re := range regexAttributes { + attr, err := findAttr(re, comment) + if err != nil { + continue + } + + switch attrKey { + case enumsTag: + err = setEnumParamV3(param.Schema.Spec, attr, objectType, schemaType) + case minimumTag, maximumTag: + err = setNumberParamV3(param.Schema.Spec, attrKey, schemaType, attr, comment) + case defaultTag: + err = setDefaultV3(param.Schema.Spec, schemaType, attr) + case minLengthTag, maxLengthTag: + err = setStringParamV3(param.Schema.Spec, attrKey, schemaType, attr, comment) + case formatTag: + param.Schema.Spec.Format = attr + case exampleTag: + val, err := defineType(schemaType, attr) + if err != nil { + continue // Don't set a example value if it's not valid + } + + param.Example = val + case schemaExampleTag: + err = setSchemaExampleV3(param.Schema.Spec, schemaType, attr) + case extensionsTag: + param.Schema.Spec.Extensions = setExtensionParam(attr) + case collectionFormatTag: + err = setCollectionFormatParamV3(param, attrKey, objectType, attr, comment) + } + + if err != nil { + return err + } + } + + return nil +} + +func (o *OperationV3) parseParamAttributeForBody(comment, objectType, schemaType string, param *spec.Schema) error { schemaType = TransToValidSchemeType(schemaType) for attrKey, re := range regexAttributes { @@ -362,15 +481,13 @@ func (o *OperationV3) parseParamAttribute(comment, objectType, schemaType string case minLengthTag, maxLengthTag: err = setStringParamV3(param, attrKey, schemaType, attr, comment) case formatTag: - param.Schema.Spec.Format = attr + param.Format = attr case exampleTag: - err = setExampleV3(param, schemaType, attr) + err = setSchemaExampleV3(param, schemaType, attr) case schemaExampleTag: err = setSchemaExampleV3(param, schemaType, attr) case extensionsTag: - param.Schema.Spec.Extensions = setExtensionParam(attr) - case collectionFormatTag: - err = setCollectionFormatParamV3(param, attrKey, objectType, attr, comment) + param.Extensions = setExtensionParam(attr) } if err != nil { @@ -390,28 +507,29 @@ func setCollectionFormatParamV3(param *spec.Parameter, name, schemaType, attr, c return fmt.Errorf("%s is attribute to set to an array. comment=%s got=%s", name, commentLine, schemaType) } -func setSchemaExampleV3(param *spec.Parameter, schemaType string, value string) error { +func setSchemaExampleV3(param *spec.Schema, schemaType string, value string) error { val, err := defineType(schemaType, value) if err != nil { return nil // Don't set a example value if it's not valid } + // skip schema - if param.Schema == nil { + if param == nil { return nil } switch v := val.(type) { case string: // replaces \r \n \t in example string values. - param.Schema.Spec.Example = strings.NewReplacer(`\r`, "\r", `\n`, "\n", `\t`, "\t").Replace(v) + param.Example = strings.NewReplacer(`\r`, "\r", `\n`, "\n", `\t`, "\t").Replace(v) default: - param.Schema.Spec.Example = val + param.Example = val } return nil } -func setExampleV3(param *spec.Parameter, schemaType string, value string) error { +func setExampleParameterV3(param *spec.Parameter, schemaType string, value string) error { val, err := defineType(schemaType, value) if err != nil { return nil // Don't set a example value if it's not valid @@ -422,7 +540,7 @@ func setExampleV3(param *spec.Parameter, schemaType string, value string) error return nil } -func setStringParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { +func setStringParamV3(param *spec.Schema, name, schemaType, attr, commentLine string) error { if schemaType != STRING { return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) } @@ -434,26 +552,26 @@ func setStringParamV3(param *spec.Parameter, name, schemaType, attr, commentLine switch name { case minLengthTag: - param.Schema.Spec.MinLength = &n + param.MinLength = &n case maxLengthTag: - param.Schema.Spec.MaxLength = &n + param.MaxLength = &n } return nil } -func setDefaultV3(param *spec.Parameter, schemaType string, value string) error { +func setDefaultV3(param *spec.Schema, schemaType string, value string) error { val, err := defineType(schemaType, value) if err != nil { return nil // Don't set a default value if it's not valid } - param.Schema.Spec.Default = val + param.Default = val return nil } -func setEnumParamV3(param *spec.Parameter, attr, objectType, schemaType string) error { +func setEnumParamV3(param *spec.Schema, attr, objectType, schemaType string) error { for _, e := range strings.Split(attr, ",") { e = strings.TrimSpace(e) @@ -464,16 +582,16 @@ func setEnumParamV3(param *spec.Parameter, attr, objectType, schemaType string) switch objectType { case ARRAY: - param.Schema.Spec.Items.Schema.Spec.Enum = append(param.Schema.Spec.Items.Schema.Spec.Enum, value) + param.Items.Schema.Spec.Enum = append(param.Items.Schema.Spec.Enum, value) default: - param.Schema.Spec.Enum = append(param.Schema.Spec.Enum, value) + param.Enum = append(param.Enum, value) } } return nil } -func setNumberParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { +func setNumberParamV3(param *spec.Schema, name, schemaType, attr, commentLine string) error { switch schemaType { case INTEGER, NUMBER: n, err := strconv.Atoi(attr) @@ -483,9 +601,9 @@ func setNumberParamV3(param *spec.Parameter, name, schemaType, attr, commentLine switch name { case minimumTag: - param.Schema.Spec.Minimum = &n + param.Minimum = &n case maximumTag: - param.Schema.Spec.Maximum = &n + param.Maximum = &n } return nil diff --git a/operationv3_test.go b/operationv3_test.go index 5b96ab043..448fb169c 100644 --- a/operationv3_test.go +++ b/operationv3_test.go @@ -863,42 +863,10 @@ func TestParseParamCommentBodyArrayV3(t *testing.T) { err := o.ParseComment(comment, nil) assert.NoError(t, err) - expected := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ - Spec: &spec.Extendable[spec.Parameter]{ - Spec: &spec.Parameter{ - Name: "names", - Description: "Users List", - In: "body", - Required: true, - Schema: &spec.RefOrSpec[spec.Schema]{ - Spec: &spec.Schema{ - JsonSchema: spec.JsonSchema{ - JsonSchemaCore: spec.JsonSchemaCore{ - Type: typeArray, - }, - JsonSchemaTypeArray: spec.JsonSchemaTypeArray{ - Items: &spec.BoolOrSchema{ - Schema: &spec.RefOrSpec[spec.Schema]{ - Spec: &spec.Schema{ - JsonSchema: spec.JsonSchema{ - JsonSchemaCore: spec.JsonSchemaCore{ - Type: typeString, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - - expectedArray := []*spec.RefOrSpec[spec.Extendable[spec.Parameter]]{expected} - assert.Equal(t, o.Parameters, expectedArray) - + assert.NotNil(t, o.RequestBody) + assert.Equal(t, "Users List", o.RequestBody.Spec.Spec.Description) + assert.True(t, o.RequestBody.Spec.Spec.Required) + assert.Equal(t, typeArray, o.RequestBody.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) } func TestParseParamCommentArrayV3(t *testing.T) { @@ -1024,16 +992,14 @@ func TestParseParamCommentByBodyTypeV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "Some ID", parameterSpec.Description) - assert.Equal(t, "some_id", parameterSpec.Name) - assert.Equal(t, true, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) - assert.Equal(t, "#/components/schemas/model.OrderRow", parameterSpec.Schema.Ref.Ref) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, "Some ID", requestBodySpec.Description) + assert.Equal(t, true, requestBodySpec.Required) + assert.Equal(t, "#/components/schemas/model.OrderRow", requestBodySpec.Content["application/json"].Spec.Schema.Ref.Ref) } func TestParseParamCommentByBodyTextPlainV3(t *testing.T) { @@ -1045,16 +1011,14 @@ func TestParseParamCommentByBodyTextPlainV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "Text to process", parameterSpec.Description) - assert.Equal(t, "text", parameterSpec.Name) - assert.Equal(t, true, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) - assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, "Text to process", requestBodySpec.Description) + assert.Equal(t, true, requestBodySpec.Required) + assert.Equal(t, typeString, requestBodySpec.Content["text/plain"].Spec.Schema.Spec.Type) } func TestParseParamCommentByBodyTypeWithDeepNestedFieldsV3(t *testing.T) { @@ -1068,19 +1032,17 @@ func TestParseParamCommentByBodyTypeWithDeepNestedFieldsV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - assert.Len(t, operation.Parameters, 1) + assert.Len(t, operation.Parameters, 0) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "test deep", parameterSpec.Description) - assert.Equal(t, "body", parameterSpec.Name) - assert.True(t, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, "test deep", requestBodySpec.Description) + assert.True(t, requestBodySpec.Required) - assert.Equal(t, 2, len(parameterSpec.Schema.Spec.AllOf)) + assert.Equal(t, 2, len(requestBodySpec.Content["application/json"].Spec.Schema.Spec.AllOf)) assert.Equal(t, 3, len(operation.parser.openAPI.Components.Spec.Schemas)) } @@ -1093,17 +1055,15 @@ func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "Some ID", parameterSpec.Description) - assert.Equal(t, "some_id", parameterSpec.Name) - assert.True(t, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) - assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) - assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Items.Schema.Spec.Type) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, "Some ID", requestBodySpec.Description) + assert.True(t, requestBodySpec.Required) + assert.Equal(t, typeArray, requestBodySpec.Content["application/json"].Spec.Schema.Spec.Type) + assert.Equal(t, typeInteger, requestBodySpec.Content["application/json"].Spec.Schema.Spec.Items.Schema.Spec.Type) } func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoWithDeepNestedFieldsV3(t *testing.T) { @@ -1116,18 +1076,15 @@ func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoWithDeepNestedFieldsV3(t * err := operation.ParseComment(comment, nil) assert.NoError(t, err) - assert.Len(t, operation.Parameters, 1) + assert.Len(t, operation.Parameters, 0) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + assert.NotNil(t, operation.RequestBody) - parameterSpec := parameters[0].Spec.Spec + parameterSpec := operation.RequestBody.Spec.Spec.Content["application/json"].Spec assert.NotNil(t, parameterSpec) - assert.Equal(t, "test deep", parameterSpec.Description) - assert.Equal(t, "body", parameterSpec.Name) - assert.True(t, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, "test deep", operation.RequestBody.Spec.Spec.Description) assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.True(t, operation.RequestBody.Spec.Spec.Required) assert.Equal(t, 2, len(parameterSpec.Schema.Spec.Items.Schema.Spec.AllOf)) } @@ -1504,23 +1461,6 @@ func TestParseParamCommentByExampleStringV3(t *testing.T) { assert.Equal(t, "True feelings", parameterSpec.Example) } -func TestParseParamCommentByExampleUnsupportedTypeV3(t *testing.T) { - t.Parallel() - var param spec.Parameter - - setExampleV3(¶m, "something", "random value") - assert.Equal(t, param.Example, nil) - - setExampleV3(¶m, STRING, "string value") - assert.Equal(t, param.Example, "string value") - - setExampleV3(¶m, INTEGER, "10") - assert.Equal(t, param.Example, 10) - - setExampleV3(¶m, NUMBER, "10") - assert.Equal(t, param.Example, float64(10)) -} - func TestParseParamCommentBySchemaExampleStringV3(t *testing.T) { t.Parallel() @@ -1530,40 +1470,38 @@ func TestParseParamCommentBySchemaExampleStringV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "Some ID", parameterSpec.Description) - assert.Equal(t, "some_id", parameterSpec.Name) - assert.True(t, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) - assert.Equal(t, "True feelings", parameterSpec.Schema.Spec.Example) - assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, "Some ID", requestBodySpec.Description) + assert.True(t, requestBodySpec.Required) + assert.Equal(t, "True feelings", requestBodySpec.Content["text/plain"].Spec.Schema.Spec.Example) + assert.Equal(t, typeString, requestBodySpec.Content["text/plain"].Spec.Schema.Spec.Type) } func TestParseParamCommentBySchemaExampleUnsupportedTypeV3(t *testing.T) { t.Parallel() var param spec.Parameter - setSchemaExampleV3(¶m, "something", "random value") + setSchemaExampleV3(nil, "something", "random value") assert.Nil(t, param.Schema) - setSchemaExampleV3(¶m, STRING, "string value") + setSchemaExampleV3(nil, STRING, "string value") assert.Nil(t, param.Schema) param.Schema = spec.NewSchemaSpec() - setSchemaExampleV3(¶m, STRING, "string value") + setSchemaExampleV3(param.Schema.Spec, STRING, "string value") assert.Equal(t, "string value", param.Schema.Spec.Example) - setSchemaExampleV3(¶m, INTEGER, "10") + setSchemaExampleV3(param.Schema.Spec, INTEGER, "10") assert.Equal(t, 10, param.Schema.Spec.Example) - setSchemaExampleV3(¶m, NUMBER, "10") + setSchemaExampleV3(param.Schema.Spec, NUMBER, "10") assert.Equal(t, float64(10), param.Schema.Spec.Example) - setSchemaExampleV3(¶m, STRING, "string \\r\\nvalue") + setSchemaExampleV3(param.Schema.Spec, STRING, "string \\r\\nvalue") assert.Equal(t, "string \r\nvalue", param.Schema.Spec.Example) } @@ -1655,7 +1593,7 @@ func TestParseAndExtractionParamAttributeV3(t *testing.T) { assert.Error(t, err) err = op.parseParamAttribute(" default(0)", "", ARRAY, nil) - assert.NoError(t, err) + assert.Error(t, err) }) } @@ -1919,3 +1857,47 @@ func TestParseCodeSamplesV3(t *testing.T) { assert.Error(t, err, "no error should be thrown") }) } + +func TestParseAcceptCommentV3(t *testing.T) { + t.Parallel() + + comment := `/@Accept json,xml,plain,html,mpfd,x-www-form-urlencoded,json-api,json-stream,octet-stream,png,jpeg,gif,application/xhtml+xml,application/health+json` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + resultMapKeys := []string{ + "application/json", + "text/xml", + "text/plain", + "text/html", + "multipart/form-data", + "application/x-www-form-urlencoded", + "application/vnd.api+json", + "application/x-json-stream", + "application/octet-stream", + "image/png", + "image/jpeg", + "image/gif", + "application/xhtml+xml", + "application/health+json"} + + content := operation.RequestBody.Spec.Spec.Content + for _, key := range resultMapKeys { + assert.NotNil(t, content[key]) + } + + assert.Equal(t, typeObject, content["application/json"].Spec.Schema.Spec.Type) + assert.Equal(t, typeObject, content["text/xml"].Spec.Schema.Spec.Type) + assert.Equal(t, typeString, content["image/png"].Spec.Schema.Spec.Type) + assert.Equal(t, "binary", content["image/png"].Spec.Schema.Spec.Format) +} + +func TestParseAcceptCommentErrV3(t *testing.T) { + t.Parallel() + + comment := `/@Accept unknown` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +} From a3046bff7e80252b8bb41487923440e3ce3db7d9 Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Tue, 11 Apr 2023 10:44:57 +0300 Subject: [PATCH 12/34] Update README_zh-CN.md (#1545) (#1546) remove repeat net/http Co-authored-by: tzxdtc10 --- README_zh-CN.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README_zh-CN.md b/README_zh-CN.md index 3ff52d0be..fc5c4e188 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -120,7 +120,6 @@ OPTIONS: - [echo](http://github.com/swaggo/echo-swagger) - [buffalo](https://github.com/swaggo/buffalo-swagger) - [net/http](https://github.com/swaggo/http-swagger) -- [net/http](https://github.com/swaggo/http-swagger) - [gorilla/mux](https://github.com/swaggo/http-swagger) - [go-chi/chi](https://github.com/swaggo/http-swagger) - [flamingo](https://github.com/i-love-flamingo/swagger) From 21247c799b63ac5d9e671f7b2da15f487a6547f5 Mon Sep 17 00:00:00 2001 From: Nerzal Date: Mon, 17 Apr 2023 18:50:23 +0200 Subject: [PATCH 13/34] V2: Support produce comment (#1551) * Implement produce comment * add tests * fix godoc --------- Co-authored-by: Tobias Theel --- operationv3.go | 91 +++++++++++++++++++++++++++++++++++++++------ operationv3_test.go | 71 ++++++++++++++++++++++++++++++++++- parserv3.go | 10 ++++- 3 files changed, 158 insertions(+), 14 deletions(-) diff --git a/operationv3.go b/operationv3.go index f4e80a497..393de54b6 100644 --- a/operationv3.go +++ b/operationv3.go @@ -20,7 +20,8 @@ type OperationV3 struct { parser *Parser codeExampleFilesDir string spec.Operation - RouterProperties []RouteProperties + RouterProperties []RouteProperties + responseMimeTypes []string } // NewOperationV3 returns a new instance of OperationV3. @@ -177,6 +178,7 @@ func (o *OperationV3) ParseAcceptComment(commentLine string) error { "application/octet-stream", "application/pdf", "application/msexcel", + "application/zip", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.presentationml.presentation": @@ -196,19 +198,84 @@ func (o *OperationV3) ParseAcceptComment(commentLine string) error { // ParseProduceComment parses comment for given `produce` comment string. func (o *OperationV3) ParseProduceComment(commentLine string) error { const errMessage = "could not parse produce comment" - // return parseMimeTypeList(commentLine, &o.Responses, "%v produce type can't be accepted") - // result, err := parseMimeTypeListV3(commentLine, "%v accept type can't be accepted") - // if err != nil { - // return errors.Wrap(err, errMessage) - // } + validTypes, err := parseMimeTypeListV3(commentLine, "%v produce type can't be accepted") + if err != nil { + return errors.Wrap(err, errMessage) + } + + o.responseMimeTypes = validTypes + + return nil +} + +// ProcessProduceComment processes the previously parsed produce comment. +func (o *OperationV3) ProcessProduceComment() error { + const errMessage = "could not process produce comment" + + if o.Responses == nil { + return nil + } + + for _, value := range o.responseMimeTypes { + if o.Responses.Spec.Response == nil { + o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]], len(o.responseMimeTypes)) + } + + for key, response := range o.Responses.Spec.Response { + code, err := strconv.Atoi(key) + if err != nil { + return errors.Wrap(err, errMessage) + } + + // Status 204 is no content. So we do not need to add content. + if code == 204 { + continue + } + + // As this is a workaround, we need to check if the code is in range. + // The Produce comment is being deprecated soon. + if code < 200 || code > 299 { + continue + } + + // skip correctly setup types like application/json + if response.Spec.Spec.Content[value] != nil { + continue + } - // for _, value := range result { - // o.Responses.Spec.Response - // } + mediaType := spec.NewMediaType() + schema := spec.NewSchemaSpec() + + switch value { + case "application/json", "multipart/form-data", "text/xml": + schema.Spec.Type = spec.NewSingleOrArray(OBJECT) + case "image/png", + "image/jpeg", + "image/gif", + "application/octet-stream", + "application/pdf", + "application/msexcel", + "application/zip", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": + schema.Spec.Type = spec.NewSingleOrArray(STRING) + schema.Spec.Format = "binary" + default: + schema.Spec.Type = spec.NewSingleOrArray(STRING) + } - // TODO the format of the comment needs to be changed in order to work - // The produce can be different per response code, so the produce mimetype needs to be included in the response comment + mediaType.Spec.Schema = schema + + if response.Spec.Spec.Content == nil { + response.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) + } + + response.Spec.Spec.Content[value] = mediaType + + } + } return nil } @@ -219,6 +286,8 @@ func (o *OperationV3) ParseProduceComment(commentLine string) error { func parseMimeTypeListV3(mimeTypeList string, format string) ([]string, error) { var result []string for _, typeName := range strings.Split(mimeTypeList, ",") { + typeName = strings.TrimSpace(typeName) + if mimeTypePattern.MatchString(typeName) { result = append(result, typeName) diff --git a/operationv3_test.go b/operationv3_test.go index 448fb169c..7e5ad86b4 100644 --- a/operationv3_test.go +++ b/operationv3_test.go @@ -1861,7 +1861,7 @@ func TestParseCodeSamplesV3(t *testing.T) { func TestParseAcceptCommentV3(t *testing.T) { t.Parallel() - comment := `/@Accept json,xml,plain,html,mpfd,x-www-form-urlencoded,json-api,json-stream,octet-stream,png,jpeg,gif,application/xhtml+xml,application/health+json` + comment := `//@Accept json,xml,plain,html,mpfd,x-www-form-urlencoded,json-api,json-stream,octet-stream,png,jpeg,gif,application/xhtml+xml,application/health+json` operation := NewOperationV3(New()) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1896,8 +1896,75 @@ func TestParseAcceptCommentV3(t *testing.T) { func TestParseAcceptCommentErrV3(t *testing.T) { t.Parallel() - comment := `/@Accept unknown` + comment := `//@Accept unknown` operation := NewOperationV3(New()) err := operation.ParseComment(comment, nil) assert.Error(t, err) } + +func TestParseProduceCommandV3(t *testing.T) { + t.Parallel() + + t.Run("Produce success", func(t *testing.T) { + t.Parallel() + + const comment = "//@Produce application/json,text/csv,application/zip" + + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + assert.Equal(t, 3, len(operation.responseMimeTypes)) + }) + + t.Run("Produce Invalid Mime Type", func(t *testing.T) { + t.Parallel() + + const comment = "//@Produce text,stuff,gophers" + + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) + }) +} + +func TestProcessProduceComment(t *testing.T) { + t.Parallel() + + const comment = "//@Produce application/json,text/csv,application/zip" + + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + require.NoError(t, err) + + operation.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]]) + operation.Responses.Spec.Response["200"] = spec.NewResponseSpec() + operation.Responses.Spec.Response["201"] = spec.NewResponseSpec() + operation.Responses.Spec.Response["204"] = spec.NewResponseSpec() + operation.Responses.Spec.Response["400"] = spec.NewResponseSpec() + operation.Responses.Spec.Response["500"] = spec.NewResponseSpec() + + err = operation.ProcessProduceComment() + require.NoError(t, err) + + content := operation.Responses.Spec.Response["200"].Spec.Spec.Content + assert.Equal(t, 3, len(content)) + assert.NotNil(t, content["application/json"].Spec.Schema) + assert.NotNil(t, content["text/csv"].Spec.Schema) + assert.NotNil(t, content["application/zip"].Spec.Schema) + + content = operation.Responses.Spec.Response["201"].Spec.Spec.Content + assert.Equal(t, 3, len(content)) + assert.NotNil(t, content["application/json"].Spec.Schema) + assert.NotNil(t, content["text/csv"].Spec.Schema) + assert.NotNil(t, content["application/zip"].Spec.Schema) + + content = operation.Responses.Spec.Response["204"].Spec.Spec.Content + assert.Nil(t, content) + + content = operation.Responses.Spec.Response["400"].Spec.Spec.Content + assert.Nil(t, content) + + content = operation.Responses.Spec.Response["500"].Spec.Spec.Content + assert.Nil(t, content) +} diff --git a/parserv3.go b/parserv3.go index 41a779135..d71e69050 100644 --- a/parserv3.go +++ b/parserv3.go @@ -442,7 +442,15 @@ func (p *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { return fmt.Errorf("ParseComment error in file %s :%+v", fileInfo.Path, err) } } - err := processRouterOperationV3(p, operation) + + // workaround until we replace the produce comment with a new @Success syntax + // We first need to setup all responses before we can set the mimetypes + err := operation.ProcessProduceComment() + if err != nil { + return err + } + + err = processRouterOperationV3(p, operation) if err != nil { return err } From 44b59ad6bc45192b97123c1ec5c15cbeb2f92e4d Mon Sep 17 00:00:00 2001 From: Nerzal Date: Mon, 17 Apr 2023 18:51:35 +0200 Subject: [PATCH 14/34] V2: upgrade modulename to v2 (#1552) * upgrade modulename to v2 * update tmpl files * change import paths * update more import paths * update more import paths * fix go.mod --------- Co-authored-by: Tobias Theel --- cmd/swag/main.go | 6 +++--- doc.go | 2 +- example/basic/api/api.go | 2 +- example/basic/docs/docs.go | 2 +- example/basic/main.go | 2 +- example/celler/controller/accounts.go | 4 ++-- example/celler/controller/admin.go | 4 ++-- example/celler/controller/bottles.go | 4 ++-- example/celler/controller/examples.go | 2 +- example/celler/go.mod | 2 +- example/celler/main.go | 6 +++--- example/markdown/docs/docs.go | 2 +- example/markdown/main.go | 7 ++++--- example/object-map-example/docs/docs.go | 2 +- example/object-map-example/main.go | 4 ++-- example/override/docs/docs.go | 2 +- format/format.go | 2 +- format/format_test.go | 2 +- gen/gen.go | 2 +- gen/gen_test.go | 2 +- gen/src/oas2.tmpl | 2 +- gen/src/oas3.tmpl | 4 ++-- go.mod | 2 +- golist_test.go | 2 +- testdata/alias_import/api/api.go | 4 ++-- testdata/alias_import/data/applicationresponse.go | 2 +- testdata/alias_import/main.go | 2 +- testdata/alias_type/api/api.go | 2 +- testdata/alias_type/data/alias.go | 3 ++- testdata/alias_type/main.go | 2 +- testdata/code_examples/api/api1.go | 3 ++- testdata/composition/api/api.go | 2 +- testdata/composition/main.go | 2 +- testdata/conflict_name/api/api1.go | 3 ++- testdata/conflict_name/api/api2.go | 3 ++- testdata/duplicated/main.go | 2 +- testdata/duplicated2/main.go | 2 +- testdata/duplicated_function_scoped/main.go | 4 ++-- testdata/enums/api/api.go | 2 +- testdata/enums/types/model.go | 2 +- testdata/error/api/api.go | 4 ++-- testdata/error/main.go | 2 +- testdata/error/web/handler.go | 2 +- testdata/format_dst/api/api.go | 2 +- testdata/format_dst/main.go | 2 +- testdata/format_dst/web/handler.go | 2 +- testdata/format_src/api/api.go | 2 +- testdata/format_src/main.go | 2 +- testdata/format_src/web/handler.go | 2 +- testdata/format_test/api/api.go | 2 +- testdata/format_test/main.go | 2 +- testdata/format_test/web/handler.go | 2 +- testdata/generics_arrays/api/api.go | 4 ++-- testdata/generics_arrays/main.go | 2 +- testdata/generics_basic/api/api.go | 4 ++-- testdata/generics_basic/main.go | 2 +- testdata/generics_multi_level_nesting/main.go | 2 +- testdata/generics_names/api/api.go | 4 ++-- testdata/generics_names/api/api_alias_pkg.go | 4 ++-- testdata/generics_names/main.go | 2 +- testdata/generics_nested/api/api.go | 4 ++-- testdata/generics_nested/main.go | 2 +- testdata/generics_package_alias/internal/api/api1.go | 2 +- testdata/generics_package_alias/internal/api/api2.go | 4 ++-- testdata/generics_package_alias/internal/api/api3.go | 4 ++-- testdata/generics_package_alias/internal/api/api4.go | 4 ++-- testdata/generics_package_alias/internal/api/api5.go | 4 ++-- testdata/generics_package_alias/internal/api/api6.go | 4 ++-- testdata/generics_package_alias/internal/api/api7.go | 4 ++-- testdata/generics_package_alias/internal/api/api8.go | 4 ++-- testdata/generics_property/api/api.go | 4 ++-- testdata/generics_property/main.go | 2 +- testdata/generics_property/web/handler.go | 2 +- testdata/global_override/api/api.go | 4 ++-- testdata/global_override/data/applicationresponse.go | 2 +- testdata/global_override/main.go | 2 +- testdata/golist/main.go | 4 ++-- testdata/golist_disablemodule/main.go | 4 ++-- testdata/golist_invalid/main.go | 4 ++-- testdata/nested/api/api.go | 2 +- testdata/nested/main.go | 2 +- testdata/pare_outside_dependencies/cmd/main.go | 2 +- testdata/quotes/main.go | 6 +++--- testdata/simple/api/api.go | 4 ++-- testdata/simple/main.go | 2 +- testdata/simple/web/handler.go | 2 +- testdata/simple2/main.go | 2 +- testdata/simple3/main.go | 2 +- testdata/simple_cgo/main.go | 2 +- testdata/struct_comment/main.go | 2 +- testdata/v3/pet/main.go | 2 +- testdata/v3/simple/api/api.go | 4 ++-- testdata/v3/simple/main.go | 2 +- testdata/v3/simple/web/handler.go | 2 +- 94 files changed, 134 insertions(+), 129 deletions(-) diff --git a/cmd/swag/main.go b/cmd/swag/main.go index f9f12e710..80c87cdb1 100644 --- a/cmd/swag/main.go +++ b/cmd/swag/main.go @@ -7,9 +7,9 @@ import ( "os" "strings" - "github.com/swaggo/swag" - "github.com/swaggo/swag/format" - "github.com/swaggo/swag/gen" + "github.com/swaggo/swag/v2" + "github.com/swaggo/swag/v2/format" + "github.com/swaggo/swag/v2/gen" "github.com/urfave/cli/v2" ) diff --git a/doc.go b/doc.go index 564b5c6f1..b57f0f38d 100644 --- a/doc.go +++ b/doc.go @@ -2,4 +2,4 @@ Package swag converts Go annotations to Swagger Documentation 2.0. See https://github.com/swaggo/swag for more information about swag. */ -package swag // import "github.com/swaggo/swag" +package swag // import "github.com/swaggo/swag/v2" diff --git a/example/basic/api/api.go b/example/basic/api/api.go index ed9e042f3..9e077a392 100644 --- a/example/basic/api/api.go +++ b/example/basic/api/api.go @@ -4,7 +4,7 @@ import ( "encoding/json" "net/http" - "github.com/swaggo/swag/example/basic/web" + "github.com/swaggo/swag/v2/example/basic/web" ) // GetStringByInt example diff --git a/example/basic/docs/docs.go b/example/basic/docs/docs.go index afa674e8f..df7e65700 100644 --- a/example/basic/docs/docs.go +++ b/example/basic/docs/docs.go @@ -2,7 +2,7 @@ // This file was generated by swaggo/swag package docs -import "github.com/swaggo/swag" +import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, diff --git a/example/basic/main.go b/example/basic/main.go index ada8112ac..1d51ccf1e 100644 --- a/example/basic/main.go +++ b/example/basic/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/example/basic/api" + "github.com/swaggo/swag/v2/example/basic/api" ) // @title Swagger Example API diff --git a/example/celler/controller/accounts.go b/example/celler/controller/accounts.go index 2815e0207..b94dda49c 100644 --- a/example/celler/controller/accounts.go +++ b/example/celler/controller/accounts.go @@ -6,8 +6,8 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/swaggo/swag/example/celler/httputil" - "github.com/swaggo/swag/example/celler/model" + "github.com/swaggo/swag/v2/example/celler/httputil" + "github.com/swaggo/swag/v2/example/celler/model" ) // ShowAccount godoc diff --git a/example/celler/controller/admin.go b/example/celler/controller/admin.go index b288d3484..a911b7fd2 100644 --- a/example/celler/controller/admin.go +++ b/example/celler/controller/admin.go @@ -6,8 +6,8 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/swaggo/swag/example/celler/httputil" - "github.com/swaggo/swag/example/celler/model" + "github.com/swaggo/swag/v2/example/celler/httputil" + "github.com/swaggo/swag/v2/example/celler/model" ) // Auth godoc diff --git a/example/celler/controller/bottles.go b/example/celler/controller/bottles.go index 1a7907d5c..6c6cfaea2 100644 --- a/example/celler/controller/bottles.go +++ b/example/celler/controller/bottles.go @@ -5,8 +5,8 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/swaggo/swag/example/celler/httputil" - "github.com/swaggo/swag/example/celler/model" + "github.com/swaggo/swag/v2/example/celler/httputil" + "github.com/swaggo/swag/v2/example/celler/model" ) // ShowBottle godoc diff --git a/example/celler/controller/examples.go b/example/celler/controller/examples.go index e27a4c3d5..39657feaf 100644 --- a/example/celler/controller/examples.go +++ b/example/celler/controller/examples.go @@ -6,7 +6,7 @@ import ( "strconv" "github.com/gin-gonic/gin" - "github.com/swaggo/swag/example/celler/httputil" + "github.com/swaggo/swag/v2/example/celler/httputil" ) // PingExample godoc diff --git a/example/celler/go.mod b/example/celler/go.mod index c9e9b260f..401c72846 100644 --- a/example/celler/go.mod +++ b/example/celler/go.mod @@ -7,7 +7,7 @@ require ( github.com/gofrs/uuid v4.2.0+incompatible github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 github.com/swaggo/gin-swagger v1.4.2 - github.com/swaggo/swag v1.8.1 + github.com/swaggo/swag/v2 v2.0.0-beta ) require ( diff --git a/example/celler/main.go b/example/celler/main.go index ec27d4757..8b4e33147 100644 --- a/example/celler/main.go +++ b/example/celler/main.go @@ -5,9 +5,9 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/swaggo/swag/example/celler/controller" - _ "github.com/swaggo/swag/example/celler/docs" - "github.com/swaggo/swag/example/celler/httputil" + "github.com/swaggo/swag/v2/example/celler/controller" + _ "github.com/swaggo/swag/v2/example/celler/docs" + "github.com/swaggo/swag/v2/example/celler/httputil" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" diff --git a/example/markdown/docs/docs.go b/example/markdown/docs/docs.go index f61835767..108b46daf 100644 --- a/example/markdown/docs/docs.go +++ b/example/markdown/docs/docs.go @@ -2,7 +2,7 @@ // This file was generated by swaggo/swag package docs -import "github.com/swaggo/swag" +import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, diff --git a/example/markdown/main.go b/example/markdown/main.go index 674fbdf8e..3e497d881 100644 --- a/example/markdown/main.go +++ b/example/markdown/main.go @@ -1,11 +1,12 @@ package main import ( + "net/http" + "github.com/gorilla/mux" httpSwagger "github.com/swaggo/http-swagger" - "github.com/swaggo/swag/example/markdown/api" - _ "github.com/swaggo/swag/example/markdown/docs" - "net/http" + "github.com/swaggo/swag/v2/example/markdown/api" + _ "github.com/swaggo/swag/v2/example/markdown/docs" ) // @title Swagger Example API diff --git a/example/object-map-example/docs/docs.go b/example/object-map-example/docs/docs.go index 3e8269210..203461490 100644 --- a/example/object-map-example/docs/docs.go +++ b/example/object-map-example/docs/docs.go @@ -2,7 +2,7 @@ // This file was generated by swaggo/swag package docs -import "github.com/swaggo/swag" +import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, diff --git a/example/object-map-example/main.go b/example/object-map-example/main.go index 7ab380892..1687ec14b 100644 --- a/example/object-map-example/main.go +++ b/example/object-map-example/main.go @@ -2,8 +2,8 @@ package main import ( "github.com/gin-gonic/gin" - "github.com/swaggo/swag/example/object-map-example/controller" - _ "github.com/swaggo/swag/example/object-map-example/docs" + "github.com/swaggo/swag/v2/example/object-map-example/controller" + _ "github.com/swaggo/swag/v2/example/object-map-example/docs" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" diff --git a/example/override/docs/docs.go b/example/override/docs/docs.go index 7b849a658..daa67e2ce 100644 --- a/example/override/docs/docs.go +++ b/example/override/docs/docs.go @@ -2,7 +2,7 @@ // This file was generated by swaggo/swag package docs -import "github.com/swaggo/swag" +import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, diff --git a/format/format.go b/format/format.go index e276be0e6..2d0aec410 100644 --- a/format/format.go +++ b/format/format.go @@ -6,7 +6,7 @@ import ( "path/filepath" "strings" - "github.com/swaggo/swag" + "github.com/swaggo/swag/v2" ) // Format implements `fmt` command for formatting swag comments in Go source diff --git a/format/format_test.go b/format/format_test.go index 152f6124a..e9eee51b3 100644 --- a/format/format_test.go +++ b/format/format_test.go @@ -118,7 +118,7 @@ var testFiles = map[string][]byte{ import ( "net/http" - "github.com/swaggo/swag/format/testdata/api" + "github.com/swaggo/swag/v2/format/testdata/api" ) // @title Swagger Example API diff --git a/gen/gen.go b/gen/gen.go index 1baea3b2a..aa3998d86 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -21,7 +21,7 @@ import ( v2 "github.com/go-openapi/spec" v3 "github.com/sv-tools/openapi/spec" - "github.com/swaggo/swag" + "github.com/swaggo/swag/v2" "sigs.k8s.io/yaml" ) diff --git a/gen/gen_test.go b/gen/gen_test.go index 9169dd355..661b67372 100644 --- a/gen/gen_test.go +++ b/gen/gen_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/swaggo/swag" + "github.com/swaggo/swag/v2" ) const searchDir = "../testdata/simple" diff --git a/gen/src/oas2.tmpl b/gen/src/oas2.tmpl index b7bce69b9..96caa3fba 100644 --- a/gen/src/oas2.tmpl +++ b/gen/src/oas2.tmpl @@ -2,7 +2,7 @@ package docs -import "github.com/swaggo/swag" +import "github.com/swaggo/swag/v2" const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = `{{ printDoc .Doc}}` diff --git a/gen/src/oas3.tmpl b/gen/src/oas3.tmpl index 20cd33af8..bc46ce7de 100644 --- a/gen/src/oas3.tmpl +++ b/gen/src/oas3.tmpl @@ -2,7 +2,7 @@ package docs -import "github.com/swaggo/swag" +import "github.com/swaggo/swag/v2" const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = `{{ printDoc .Doc}}` @@ -17,4 +17,4 @@ var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} func init() { swag.Register(SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}) -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 5a6271167..32f79d171 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/swaggo/swag +module github.com/swaggo/swag/v2 go 1.18 diff --git a/golist_test.go b/golist_test.go index 6b11da7b7..cd0a550f2 100644 --- a/golist_test.go +++ b/golist_test.go @@ -66,7 +66,7 @@ func TestGetAllGoFileInfoFromDepsByList(t *testing.T) { name: "normal", buildPackage: &build.Package{ Name: "main", - ImportPath: "github.com/swaggo/swag/testdata/golist", + ImportPath: "github.com/swaggo/swag/v2/testdata/golist", Dir: "testdata/golist", GoFiles: []string{"main.go"}, CgoFiles: []string{"api/api.go"}, diff --git a/testdata/alias_import/api/api.go b/testdata/alias_import/api/api.go index a01a3ddd5..372c44a4b 100644 --- a/testdata/alias_import/api/api.go +++ b/testdata/alias_import/api/api.go @@ -4,8 +4,8 @@ import ( "log" "net/http" - "github.com/swaggo/swag/testdata/alias_import/data" - "github.com/swaggo/swag/testdata/alias_type/types" + "github.com/swaggo/swag/v2/testdata/alias_import/data" + "github.com/swaggo/swag/v2/testdata/alias_type/types" ) // @Summary Get application diff --git a/testdata/alias_import/data/applicationresponse.go b/testdata/alias_import/data/applicationresponse.go index da4170508..6c3dbebd6 100644 --- a/testdata/alias_import/data/applicationresponse.go +++ b/testdata/alias_import/data/applicationresponse.go @@ -1,7 +1,7 @@ package data import ( - typesapplication "github.com/swaggo/swag/testdata/alias_import/types" + typesapplication "github.com/swaggo/swag/v2/testdata/alias_import/types" ) type ApplicationResponse struct { diff --git a/testdata/alias_import/main.go b/testdata/alias_import/main.go index bf1b5c93b..e5fcdea7a 100644 --- a/testdata/alias_import/main.go +++ b/testdata/alias_import/main.go @@ -3,7 +3,7 @@ package alias_import import ( "net/http" - "github.com/swaggo/swag/testdata/alias_import/api" + "github.com/swaggo/swag/v2/testdata/alias_import/api" ) // @title Swagger Example API diff --git a/testdata/alias_type/api/api.go b/testdata/alias_type/api/api.go index 3c9638e06..cb2aae0be 100644 --- a/testdata/alias_type/api/api.go +++ b/testdata/alias_type/api/api.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - "github.com/swaggo/swag/testdata/alias_type/data" + "github.com/swaggo/swag/v2/testdata/alias_type/data" ) /*// @Summary Get time as string diff --git a/testdata/alias_type/data/alias.go b/testdata/alias_type/data/alias.go index 24051055f..9eef9d8ae 100644 --- a/testdata/alias_type/data/alias.go +++ b/testdata/alias_type/data/alias.go @@ -1,8 +1,9 @@ package data import ( - "github.com/swaggo/swag/testdata/alias_type/types" "time" + + "github.com/swaggo/swag/v2/testdata/alias_type/types" ) type TimeContainer struct { diff --git a/testdata/alias_type/main.go b/testdata/alias_type/main.go index f136fbb95..e1bf075bf 100644 --- a/testdata/alias_type/main.go +++ b/testdata/alias_type/main.go @@ -3,7 +3,7 @@ package alias_type import ( "net/http" - "github.com/swaggo/swag/testdata/alias_type/api" + "github.com/swaggo/swag/v2/testdata/alias_type/api" ) // @title Swagger Example API diff --git a/testdata/code_examples/api/api1.go b/testdata/code_examples/api/api1.go index 4bbd5512f..b0bfed89b 100644 --- a/testdata/code_examples/api/api1.go +++ b/testdata/code_examples/api/api1.go @@ -1,8 +1,9 @@ package api import ( - _ "github.com/swaggo/swag/testdata/conflict_name/model" "net/http" + + _ "github.com/swaggo/swag/v2/testdata/conflict_name/model" ) // @Description Check if Health of service it's OK! diff --git a/testdata/composition/api/api.go b/testdata/composition/api/api.go index bd3c62487..cf0efba17 100644 --- a/testdata/composition/api/api.go +++ b/testdata/composition/api/api.go @@ -3,7 +3,7 @@ package api import ( "net/http" - "github.com/swaggo/swag/testdata/composition/common" + "github.com/swaggo/swag/v2/testdata/composition/common" ) type Foo struct { diff --git a/testdata/composition/main.go b/testdata/composition/main.go index e0dc20d64..91c294652 100644 --- a/testdata/composition/main.go +++ b/testdata/composition/main.go @@ -3,7 +3,7 @@ package composition import ( "net/http" - "github.com/swaggo/swag/testdata/composition/api" + "github.com/swaggo/swag/v2/testdata/composition/api" ) // @title Swagger Example API diff --git a/testdata/conflict_name/api/api1.go b/testdata/conflict_name/api/api1.go index 7b04198a7..25f63bc1b 100644 --- a/testdata/conflict_name/api/api1.go +++ b/testdata/conflict_name/api/api1.go @@ -1,8 +1,9 @@ package api import ( - _ "github.com/swaggo/swag/testdata/conflict_name/model" "net/http" + + _ "github.com/swaggo/swag/v2/testdata/conflict_name/model" ) // @Tags Health diff --git a/testdata/conflict_name/api/api2.go b/testdata/conflict_name/api/api2.go index e71b7e6c1..9073016ab 100644 --- a/testdata/conflict_name/api/api2.go +++ b/testdata/conflict_name/api/api2.go @@ -1,8 +1,9 @@ package api import ( - _ "github.com/swaggo/swag/testdata/conflict_name/model2" "net/http" + + _ "github.com/swaggo/swag/v2/testdata/conflict_name/model2" ) // @Tags Health diff --git a/testdata/duplicated/main.go b/testdata/duplicated/main.go index d1e16664f..054ceec7f 100644 --- a/testdata/duplicated/main.go +++ b/testdata/duplicated/main.go @@ -3,7 +3,7 @@ package composition import ( "net/http" - "github.com/swaggo/swag/testdata/duplicated/api" + "github.com/swaggo/swag/v2/testdata/duplicated/api" ) // @title Swagger Example API diff --git a/testdata/duplicated2/main.go b/testdata/duplicated2/main.go index 90988f76d..f46a62496 100644 --- a/testdata/duplicated2/main.go +++ b/testdata/duplicated2/main.go @@ -3,7 +3,7 @@ package composition import ( "net/http" - "github.com/swaggo/swag/testdata/duplicated2/api" + "github.com/swaggo/swag/v2/testdata/duplicated2/api" ) // @title Swagger Example API diff --git a/testdata/duplicated_function_scoped/main.go b/testdata/duplicated_function_scoped/main.go index d5f34680f..cc3b7d745 100644 --- a/testdata/duplicated_function_scoped/main.go +++ b/testdata/duplicated_function_scoped/main.go @@ -3,8 +3,8 @@ 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" + "github.com/swaggo/swag/v2/testdata/duplicated_function_scoped/api" + otherapi "github.com/swaggo/swag/v2/testdata/duplicated_function_scoped/other_api" ) // @title Swagger Example API diff --git a/testdata/enums/api/api.go b/testdata/enums/api/api.go index 5d2d6dbd3..435b7beee 100644 --- a/testdata/enums/api/api.go +++ b/testdata/enums/api/api.go @@ -1,6 +1,6 @@ package api -import "github.com/swaggo/swag/testdata/enums/types" +import "github.com/swaggo/swag/v2/testdata/enums/types" // post students // diff --git a/testdata/enums/types/model.go b/testdata/enums/types/model.go index 8fc6bbc66..47822811e 100644 --- a/testdata/enums/types/model.go +++ b/testdata/enums/types/model.go @@ -1,7 +1,7 @@ package types import ( - "github.com/swaggo/swag/testdata/enums/consts" + "github.com/swaggo/swag/v2/testdata/enums/consts" ) type Class int diff --git a/testdata/error/api/api.go b/testdata/error/api/api.go index 8c59da5c7..bc8af8143 100644 --- a/testdata/error/api/api.go +++ b/testdata/error/api/api.go @@ -3,8 +3,8 @@ package api import ( "net/http" - . "github.com/swaggo/swag/testdata/error/errors" - _ "github.com/swaggo/swag/testdata/error/web" + . "github.com/swaggo/swag/v2/testdata/error/errors" + _ "github.com/swaggo/swag/v2/testdata/error/web" ) // Upload do something diff --git a/testdata/error/main.go b/testdata/error/main.go index 249c0bb85..1929ed8f6 100644 --- a/testdata/error/main.go +++ b/testdata/error/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/error/api" + "github.com/swaggo/swag/v2/testdata/error/api" ) // @title Swagger Example API diff --git a/testdata/error/web/handler.go b/testdata/error/web/handler.go index c46f90a9d..67fe14275 100644 --- a/testdata/error/web/handler.go +++ b/testdata/error/web/handler.go @@ -1,7 +1,7 @@ package web import ( - "github.com/swaggo/swag/testdata/error/errors" + "github.com/swaggo/swag/v2/testdata/error/errors" ) type CrossErrors errors.Errors diff --git a/testdata/format_dst/api/api.go b/testdata/format_dst/api/api.go index 91cd19cce..c7d1e8369 100644 --- a/testdata/format_dst/api/api.go +++ b/testdata/format_dst/api/api.go @@ -3,7 +3,7 @@ package api import ( "net/http" - _ "github.com/swaggo/swag/testdata/simple/web" + _ "github.com/swaggo/swag/v2/testdata/simple/web" ) // @Summary Add a new pet to the store diff --git a/testdata/format_dst/main.go b/testdata/format_dst/main.go index be5481f93..c4dc7fdd8 100644 --- a/testdata/format_dst/main.go +++ b/testdata/format_dst/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/simple/api" + "github.com/swaggo/swag/v2/testdata/simple/api" ) // @title Swagger Example API diff --git a/testdata/format_dst/web/handler.go b/testdata/format_dst/web/handler.go index 78c967182..192ffdb11 100644 --- a/testdata/format_dst/web/handler.go +++ b/testdata/format_dst/web/handler.go @@ -6,7 +6,7 @@ import ( "github.com/gofrs/uuid" "github.com/shopspring/decimal" - "github.com/swaggo/swag/testdata/simple/cross" + "github.com/swaggo/swag/v2/testdata/simple/cross" ) type Pet struct { diff --git a/testdata/format_src/api/api.go b/testdata/format_src/api/api.go index 9288cf6c8..3a84555cd 100644 --- a/testdata/format_src/api/api.go +++ b/testdata/format_src/api/api.go @@ -3,7 +3,7 @@ package api import ( "net/http" - _ "github.com/swaggo/swag/testdata/simple/web" + _ "github.com/swaggo/swag/v2/testdata/simple/web" ) // @Summary Add a new pet to the store diff --git a/testdata/format_src/main.go b/testdata/format_src/main.go index 824a017c5..3b6e9c353 100644 --- a/testdata/format_src/main.go +++ b/testdata/format_src/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/simple/api" + "github.com/swaggo/swag/v2/testdata/simple/api" ) // @title Swagger Example API diff --git a/testdata/format_src/web/handler.go b/testdata/format_src/web/handler.go index 0bfadc957..6c45a512c 100644 --- a/testdata/format_src/web/handler.go +++ b/testdata/format_src/web/handler.go @@ -6,7 +6,7 @@ import ( "github.com/gofrs/uuid" "github.com/shopspring/decimal" - "github.com/swaggo/swag/testdata/simple/cross" + "github.com/swaggo/swag/v2/testdata/simple/cross" ) type Pet struct { diff --git a/testdata/format_test/api/api.go b/testdata/format_test/api/api.go index 9288cf6c8..3a84555cd 100644 --- a/testdata/format_test/api/api.go +++ b/testdata/format_test/api/api.go @@ -3,7 +3,7 @@ package api import ( "net/http" - _ "github.com/swaggo/swag/testdata/simple/web" + _ "github.com/swaggo/swag/v2/testdata/simple/web" ) // @Summary Add a new pet to the store diff --git a/testdata/format_test/main.go b/testdata/format_test/main.go index 824a017c5..3b6e9c353 100644 --- a/testdata/format_test/main.go +++ b/testdata/format_test/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/simple/api" + "github.com/swaggo/swag/v2/testdata/simple/api" ) // @title Swagger Example API diff --git a/testdata/format_test/web/handler.go b/testdata/format_test/web/handler.go index 0bfadc957..6c45a512c 100644 --- a/testdata/format_test/web/handler.go +++ b/testdata/format_test/web/handler.go @@ -6,7 +6,7 @@ import ( "github.com/gofrs/uuid" "github.com/shopspring/decimal" - "github.com/swaggo/swag/testdata/simple/cross" + "github.com/swaggo/swag/v2/testdata/simple/cross" ) type Pet struct { diff --git a/testdata/generics_arrays/api/api.go b/testdata/generics_arrays/api/api.go index 66ef1bb02..8f47f55ce 100644 --- a/testdata/generics_arrays/api/api.go +++ b/testdata/generics_arrays/api/api.go @@ -3,8 +3,8 @@ package api import ( "net/http" - "github.com/swaggo/swag/testdata/generics_arrays/types" - "github.com/swaggo/swag/testdata/generics_arrays/web" + "github.com/swaggo/swag/v2/testdata/generics_arrays/types" + "github.com/swaggo/swag/v2/testdata/generics_arrays/web" ) // @Summary List Posts diff --git a/testdata/generics_arrays/main.go b/testdata/generics_arrays/main.go index 1e5423ecd..c16137dc5 100644 --- a/testdata/generics_arrays/main.go +++ b/testdata/generics_arrays/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/generics_arrays/api" + "github.com/swaggo/swag/v2/testdata/generics_arrays/api" ) // @title Swagger Example API diff --git a/testdata/generics_basic/api/api.go b/testdata/generics_basic/api/api.go index eecd72f03..bc58fc515 100644 --- a/testdata/generics_basic/api/api.go +++ b/testdata/generics_basic/api/api.go @@ -3,8 +3,8 @@ package api import ( "net/http" - "github.com/swaggo/swag/testdata/generics_basic/types" - "github.com/swaggo/swag/testdata/generics_basic/web" + "github.com/swaggo/swag/v2/testdata/generics_basic/types" + "github.com/swaggo/swag/v2/testdata/generics_basic/web" ) type Response[T any, X any] struct { diff --git a/testdata/generics_basic/main.go b/testdata/generics_basic/main.go index 99845ab89..596ae09c9 100644 --- a/testdata/generics_basic/main.go +++ b/testdata/generics_basic/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/generics_basic/api" + "github.com/swaggo/swag/v2/testdata/generics_basic/api" ) // @title Swagger Example API diff --git a/testdata/generics_multi_level_nesting/main.go b/testdata/generics_multi_level_nesting/main.go index fd5981374..67943cd50 100644 --- a/testdata/generics_multi_level_nesting/main.go +++ b/testdata/generics_multi_level_nesting/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/generics_nested_my_version/api" + "github.com/swaggo/swag/v2/testdata/generics_nested_my_version/api" ) // @title Swagger Example API diff --git a/testdata/generics_names/api/api.go b/testdata/generics_names/api/api.go index 7c30be9de..d5087d6b0 100644 --- a/testdata/generics_names/api/api.go +++ b/testdata/generics_names/api/api.go @@ -3,8 +3,8 @@ package api import ( "net/http" - "github.com/swaggo/swag/testdata/generics_names/types" - "github.com/swaggo/swag/testdata/generics_names/web" + "github.com/swaggo/swag/v2/testdata/generics_names/types" + "github.com/swaggo/swag/v2/testdata/generics_names/web" ) // @Summary Add a new pet to the store diff --git a/testdata/generics_names/api/api_alias_pkg.go b/testdata/generics_names/api/api_alias_pkg.go index 2ae39d69a..4acf22b90 100644 --- a/testdata/generics_names/api/api_alias_pkg.go +++ b/testdata/generics_names/api/api_alias_pkg.go @@ -3,8 +3,8 @@ package api import ( "net/http" - mytypes "github.com/swaggo/swag/testdata/generics_names/types" - myweb "github.com/swaggo/swag/testdata/generics_names/web" + mytypes "github.com/swaggo/swag/v2/testdata/generics_names/types" + myweb "github.com/swaggo/swag/v2/testdata/generics_names/web" ) // @Summary Add a new pet to the store diff --git a/testdata/generics_names/main.go b/testdata/generics_names/main.go index bf7307b9d..4d91f6383 100644 --- a/testdata/generics_names/main.go +++ b/testdata/generics_names/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/generics_names/api" + "github.com/swaggo/swag/v2/testdata/generics_names/api" ) // @title Swagger Example API diff --git a/testdata/generics_nested/api/api.go b/testdata/generics_nested/api/api.go index 19c215ee6..cfd95170c 100644 --- a/testdata/generics_nested/api/api.go +++ b/testdata/generics_nested/api/api.go @@ -3,8 +3,8 @@ package api import ( "net/http" - "github.com/swaggo/swag/testdata/generics_nested/types" - "github.com/swaggo/swag/testdata/generics_nested/web" + "github.com/swaggo/swag/v2/testdata/generics_nested/types" + "github.com/swaggo/swag/v2/testdata/generics_nested/web" ) // @Summary List Posts diff --git a/testdata/generics_nested/main.go b/testdata/generics_nested/main.go index 4817e03cb..3917c5779 100644 --- a/testdata/generics_nested/main.go +++ b/testdata/generics_nested/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/generics_nested/api" + "github.com/swaggo/swag/v2/testdata/generics_nested/api" ) // @title Swagger Example API diff --git a/testdata/generics_package_alias/internal/api/api1.go b/testdata/generics_package_alias/internal/api/api1.go index 4152ab313..341a0301f 100644 --- a/testdata/generics_package_alias/internal/api/api1.go +++ b/testdata/generics_package_alias/internal/api/api1.go @@ -1,7 +1,7 @@ package api import ( - myv1 "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" + myv1 "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path1/v1" ) // @Summary Create movie diff --git a/testdata/generics_package_alias/internal/api/api2.go b/testdata/generics_package_alias/internal/api/api2.go index d0cb09610..e8281d92f 100644 --- a/testdata/generics_package_alias/internal/api/api2.go +++ b/testdata/generics_package_alias/internal/api/api2.go @@ -1,8 +1,8 @@ package api import ( - myv1 "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" - myv2 "github.com/swaggo/swag/testdata/generics_package_alias/internal/path2/v1" + myv1 "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path1/v1" + myv2 "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path2/v1" ) // @Summary Create movie diff --git a/testdata/generics_package_alias/internal/api/api3.go b/testdata/generics_package_alias/internal/api/api3.go index f269d734f..66c667102 100644 --- a/testdata/generics_package_alias/internal/api/api3.go +++ b/testdata/generics_package_alias/internal/api/api3.go @@ -1,8 +1,8 @@ package api import ( - _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" - . "github.com/swaggo/swag/testdata/generics_package_alias/internal/path2/v1" + _ "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path1/v1" + . "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path2/v1" ) // @Summary Create movie diff --git a/testdata/generics_package_alias/internal/api/api4.go b/testdata/generics_package_alias/internal/api/api4.go index 9a851ba04..b9a06f56c 100644 --- a/testdata/generics_package_alias/internal/api/api4.go +++ b/testdata/generics_package_alias/internal/api/api4.go @@ -1,8 +1,8 @@ package api import ( - "github.com/swaggo/swag/testdata/generics_package_alias/external/external1" - _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" + "github.com/swaggo/swag/v2/testdata/generics_package_alias/external/external1" + _ "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path1/v1" ) // @Summary Create movie diff --git a/testdata/generics_package_alias/internal/api/api5.go b/testdata/generics_package_alias/internal/api/api5.go index c15272709..2d5d151d2 100644 --- a/testdata/generics_package_alias/internal/api/api5.go +++ b/testdata/generics_package_alias/internal/api/api5.go @@ -1,8 +1,8 @@ package api import ( - myexternal "github.com/swaggo/swag/testdata/generics_package_alias/external/external2" - _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" + myexternal "github.com/swaggo/swag/v2/testdata/generics_package_alias/external/external2" + _ "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path1/v1" ) // @Summary Create movie diff --git a/testdata/generics_package_alias/internal/api/api6.go b/testdata/generics_package_alias/internal/api/api6.go index 5d1d27072..85d7c1121 100644 --- a/testdata/generics_package_alias/internal/api/api6.go +++ b/testdata/generics_package_alias/internal/api/api6.go @@ -1,8 +1,8 @@ package api import ( - . "github.com/swaggo/swag/testdata/generics_package_alias/external/external3" - _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" + . "github.com/swaggo/swag/v2/testdata/generics_package_alias/external/external3" + _ "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path1/v1" ) // @Summary Create movie diff --git a/testdata/generics_package_alias/internal/api/api7.go b/testdata/generics_package_alias/internal/api/api7.go index 5a5ad72c4..2f4caf1a2 100644 --- a/testdata/generics_package_alias/internal/api/api7.go +++ b/testdata/generics_package_alias/internal/api/api7.go @@ -1,8 +1,8 @@ package api import ( - _ "github.com/swaggo/swag/testdata/generics_package_alias/external/external4" - _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" + _ "github.com/swaggo/swag/v2/testdata/generics_package_alias/external/external4" + _ "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path1/v1" ) // @Summary Create movie diff --git a/testdata/generics_package_alias/internal/api/api8.go b/testdata/generics_package_alias/internal/api/api8.go index bcdd94715..5979d15da 100644 --- a/testdata/generics_package_alias/internal/api/api8.go +++ b/testdata/generics_package_alias/internal/api/api8.go @@ -1,8 +1,8 @@ package api import ( - _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" - _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path2/v1" + _ "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path1/v1" + _ "github.com/swaggo/swag/v2/testdata/generics_package_alias/internal/path2/v1" ) // @Summary Create movie diff --git a/testdata/generics_property/api/api.go b/testdata/generics_property/api/api.go index 2ef9198c1..c7e2b9af5 100644 --- a/testdata/generics_property/api/api.go +++ b/testdata/generics_property/api/api.go @@ -3,8 +3,8 @@ package api import ( "net/http" - "github.com/swaggo/swag/testdata/generics_property/types" - "github.com/swaggo/swag/testdata/generics_property/web" + "github.com/swaggo/swag/v2/testdata/generics_property/types" + "github.com/swaggo/swag/v2/testdata/generics_property/web" ) type NestedResponse struct { diff --git a/testdata/generics_property/main.go b/testdata/generics_property/main.go index 41b58c6e3..15aad1fe9 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_property/api" + "github.com/swaggo/swag/v2/testdata/generics_property/api" ) // @title Swagger Example API diff --git a/testdata/generics_property/web/handler.go b/testdata/generics_property/web/handler.go index e22aef025..725244737 100644 --- a/testdata/generics_property/web/handler.go +++ b/testdata/generics_property/web/handler.go @@ -1,6 +1,6 @@ package web -import "github.com/swaggo/swag/testdata/generics_property/types" +import "github.com/swaggo/swag/v2/testdata/generics_property/types" type PostSelector func(selector func()) diff --git a/testdata/global_override/api/api.go b/testdata/global_override/api/api.go index c619dfd1b..b40f4ff5a 100644 --- a/testdata/global_override/api/api.go +++ b/testdata/global_override/api/api.go @@ -4,8 +4,8 @@ import ( "log" "net/http" - "github.com/swaggo/swag/testdata/alias_type/types" - "github.com/swaggo/swag/testdata/global_override/data" + "github.com/swaggo/swag/v2/testdata/alias_type/types" + "github.com/swaggo/swag/v2/testdata/global_override/data" ) // @Summary Get application diff --git a/testdata/global_override/data/applicationresponse.go b/testdata/global_override/data/applicationresponse.go index 097e80364..47839f9a3 100644 --- a/testdata/global_override/data/applicationresponse.go +++ b/testdata/global_override/data/applicationresponse.go @@ -1,7 +1,7 @@ package data import ( - typesapplication "github.com/swaggo/swag/testdata/global_override/types" + typesapplication "github.com/swaggo/swag/v2/testdata/global_override/types" ) type ApplicationResponse struct { diff --git a/testdata/global_override/main.go b/testdata/global_override/main.go index c77a6c6ca..306b0e10c 100644 --- a/testdata/global_override/main.go +++ b/testdata/global_override/main.go @@ -3,7 +3,7 @@ package global_override import ( "net/http" - "github.com/swaggo/swag/testdata/global_override/api" + "github.com/swaggo/swag/v2/testdata/global_override/api" ) // @title Swagger Example API diff --git a/testdata/golist/main.go b/testdata/golist/main.go index 70143eb88..e05eb1132 100644 --- a/testdata/golist/main.go +++ b/testdata/golist/main.go @@ -3,8 +3,8 @@ package main import ( "net/http" - "github.com/swaggo/swag/example/basic/api" - goapi "github.com/swaggo/swag/testdata/golist/api" + "github.com/swaggo/swag/v2/example/basic/api" + goapi "github.com/swaggo/swag/v2/testdata/golist/api" ) // @title Swagger Example API diff --git a/testdata/golist_disablemodule/main.go b/testdata/golist_disablemodule/main.go index ab7d61735..864327167 100644 --- a/testdata/golist_disablemodule/main.go +++ b/testdata/golist_disablemodule/main.go @@ -3,8 +3,8 @@ package main import ( "net/http" - "github.com/swaggo/swag/example/basic/api" - internalapi "github.com/swaggo/swag/testdata/golist_disablemodule/api" + "github.com/swaggo/swag/v2/example/basic/api" + internalapi "github.com/swaggo/swag/v2/testdata/golist_disablemodule/api" ) // @title Swagger Example API diff --git a/testdata/golist_invalid/main.go b/testdata/golist_invalid/main.go index 69bae8025..251deaac7 100644 --- a/testdata/golist_invalid/main.go +++ b/testdata/golist_invalid/main.go @@ -3,8 +3,8 @@ package main import ( "net/http" - "github.com/swaggo/swag/example/basic/api" - "github.com/swaggo/swag/testdata/invalid_external_pkg/invalid" + "github.com/swaggo/swag/v2/example/basic/api" + "github.com/swaggo/swag/v2/testdata/invalid_external_pkg/invalid" ) // @title Swagger Example API diff --git a/testdata/nested/api/api.go b/testdata/nested/api/api.go index 0dec600ac..d5cbf6f9d 100644 --- a/testdata/nested/api/api.go +++ b/testdata/nested/api/api.go @@ -3,7 +3,7 @@ package api import ( "net/http" - "github.com/swaggo/swag/testdata/nested2" + "github.com/swaggo/swag/v2/testdata/nested2" ) type Foo struct { diff --git a/testdata/nested/main.go b/testdata/nested/main.go index 6e63913b3..577c45cff 100644 --- a/testdata/nested/main.go +++ b/testdata/nested/main.go @@ -3,7 +3,7 @@ package composition import ( "net/http" - "github.com/swaggo/swag/testdata/nested/api" + "github.com/swaggo/swag/v2/testdata/nested/api" ) // @title Swagger Example API diff --git a/testdata/pare_outside_dependencies/cmd/main.go b/testdata/pare_outside_dependencies/cmd/main.go index 9f22eb586..84d054543 100644 --- a/testdata/pare_outside_dependencies/cmd/main.go +++ b/testdata/pare_outside_dependencies/cmd/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/example/basic/api" + "github.com/swaggo/swag/v2/example/basic/api" ) // @title Swagger Example API diff --git a/testdata/quotes/main.go b/testdata/quotes/main.go index ec2da63fb..ec723375f 100644 --- a/testdata/quotes/main.go +++ b/testdata/quotes/main.go @@ -1,9 +1,9 @@ package main import ( - "github.com/swaggo/swag" - "github.com/swaggo/swag/testdata/quotes/api" - _ "github.com/swaggo/swag/testdata/quotes/docs" + "github.com/swaggo/swag/v2" + "github.com/swaggo/swag/v2/testdata/quotes/api" + _ "github.com/swaggo/swag/v2/testdata/quotes/docs" ) func ReadDoc() string { diff --git a/testdata/simple/api/api.go b/testdata/simple/api/api.go index 85a7fa48f..7eee24347 100644 --- a/testdata/simple/api/api.go +++ b/testdata/simple/api/api.go @@ -3,8 +3,8 @@ package api import ( "net/http" - . "github.com/swaggo/swag/testdata/simple/cross" - _ "github.com/swaggo/swag/testdata/simple/web" + . "github.com/swaggo/swag/v2/testdata/simple/cross" + _ "github.com/swaggo/swag/v2/testdata/simple/web" ) // @Summary Add a new pet to the store diff --git a/testdata/simple/main.go b/testdata/simple/main.go index 824a017c5..3b6e9c353 100644 --- a/testdata/simple/main.go +++ b/testdata/simple/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/simple/api" + "github.com/swaggo/swag/v2/testdata/simple/api" ) // @title Swagger Example API diff --git a/testdata/simple/web/handler.go b/testdata/simple/web/handler.go index 09ee251e7..4f908baf8 100644 --- a/testdata/simple/web/handler.go +++ b/testdata/simple/web/handler.go @@ -5,7 +5,7 @@ import ( "github.com/gofrs/uuid" "github.com/shopspring/decimal" - "github.com/swaggo/swag/testdata/simple/cross" + "github.com/swaggo/swag/v2/testdata/simple/cross" ) type Pet struct { diff --git a/testdata/simple2/main.go b/testdata/simple2/main.go index 5858e02eb..9f5ba73dd 100644 --- a/testdata/simple2/main.go +++ b/testdata/simple2/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/simple2/api" + "github.com/swaggo/swag/v2/testdata/simple2/api" ) // @title Swagger Example API diff --git a/testdata/simple3/main.go b/testdata/simple3/main.go index 307cff1dd..851a486f5 100644 --- a/testdata/simple3/main.go +++ b/testdata/simple3/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/simple3/api" + "github.com/swaggo/swag/v2/testdata/simple3/api" ) // @title Swagger Example API diff --git a/testdata/simple_cgo/main.go b/testdata/simple_cgo/main.go index 971ae70ed..0c26a635f 100644 --- a/testdata/simple_cgo/main.go +++ b/testdata/simple_cgo/main.go @@ -12,7 +12,7 @@ import "C" import ( "net/http" - "github.com/swaggo/swag/testdata/simple_cgo/api" + "github.com/swaggo/swag/v2/testdata/simple_cgo/api" ) // @title Swagger Example API diff --git a/testdata/struct_comment/main.go b/testdata/struct_comment/main.go index 6994817b5..dc9eef900 100644 --- a/testdata/struct_comment/main.go +++ b/testdata/struct_comment/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/struct_comment/api" + "github.com/swaggo/swag/v2/testdata/struct_comment/api" ) // @title Swagger Example API diff --git a/testdata/v3/pet/main.go b/testdata/v3/pet/main.go index 90182a2e6..4aa462073 100644 --- a/testdata/v3/pet/main.go +++ b/testdata/v3/pet/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/v3/pet/web" + "github.com/swaggo/swag/v2/testdata/v3/pet/web" ) // @title Swagger Petstore diff --git a/testdata/v3/simple/api/api.go b/testdata/v3/simple/api/api.go index 324385e58..116d5e86a 100644 --- a/testdata/v3/simple/api/api.go +++ b/testdata/v3/simple/api/api.go @@ -3,8 +3,8 @@ package api import ( "net/http" - . "github.com/swaggo/swag/testdata/v3/simple/cross" - _ "github.com/swaggo/swag/testdata/v3/simple/web" + . "github.com/swaggo/swag/v2/testdata/v3/simple/cross" + _ "github.com/swaggo/swag/v2/testdata/v3/simple/web" ) // @Summary Add a new pet to the store diff --git a/testdata/v3/simple/main.go b/testdata/v3/simple/main.go index a7cd1447a..0b960233b 100644 --- a/testdata/v3/simple/main.go +++ b/testdata/v3/simple/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/swaggo/swag/testdata/v3/simple/api" + "github.com/swaggo/swag/v2/testdata/v3/simple/api" ) // @title Swagger Example API diff --git a/testdata/v3/simple/web/handler.go b/testdata/v3/simple/web/handler.go index 546fe322c..e67a67f40 100644 --- a/testdata/v3/simple/web/handler.go +++ b/testdata/v3/simple/web/handler.go @@ -5,7 +5,7 @@ import ( "github.com/gofrs/uuid" "github.com/shopspring/decimal" - "github.com/swaggo/swag/testdata/v3/simple/cross" + "github.com/swaggo/swag/v2/testdata/v3/simple/cross" ) type Pet struct { From 543e18b21983037f8bc9fac54762d8f01f6d887b Mon Sep 17 00:00:00 2001 From: Nerzal Date: Mon, 17 Apr 2023 18:59:27 +0200 Subject: [PATCH 15/34] implement pattern tag (#1553) Co-authored-by: Tobias Theel --- field_parser.go | 8 + field_parser_test.go | 44 +++ field_parser_v3_test.go | 666 ++++++++++++++++++++++++++++++++++++++++ field_parserv3.go | 7 + operation.go | 1 + 5 files changed, 726 insertions(+) create mode 100644 field_parser_v3_test.go diff --git a/field_parser.go b/field_parser.go index 1716e5034..f7d3fed65 100644 --- a/field_parser.go +++ b/field_parser.go @@ -173,6 +173,7 @@ type structField struct { enums []interface{} enumVarNames []interface{} unique bool + pattern string } // splitNotWrapped slices s into all substrings separated by sep if sep is not @@ -337,6 +338,11 @@ func (ps *tagBaseFieldParser) complementSchema(schema *spec.Schema, types []stri if minLength != nil { field.minLength = minLength } + + pattern, ok := ps.tag.Lookup(patternTag) + if ok { + field.pattern = pattern + } } // json:"name,string" or json:",string" @@ -445,6 +451,7 @@ func (ps *tagBaseFieldParser) complementSchema(schema *spec.Schema, types []stri schema.MaxItems = field.maxItems schema.MinItems = field.minItems schema.UniqueItems = field.unique + schema.Pattern = field.pattern eleSchema = schema.Items.Schema eleSchema.Format = field.formatType @@ -456,6 +463,7 @@ func (ps *tagBaseFieldParser) complementSchema(schema *spec.Schema, types []stri eleSchema.MaxLength = field.maxLength eleSchema.MinLength = field.minLength eleSchema.Enum = field.enums + eleSchema.Pattern = field.pattern return nil } diff --git a/field_parser_test.go b/field_parser_test.go index a7487c03d..6f4bfdef3 100644 --- a/field_parser_test.go +++ b/field_parser_test.go @@ -668,4 +668,48 @@ func TestValidTags(t *testing.T) { assert.NoError(t, err) assert.Empty(t, schema.Enum) }) + + t.Run("Pattern tag", func(t *testing.T) { + t.Parallel() + + schema := spec.Schema{} + schema.Type = []string{"array"} + schema.Items = &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + } + err := newTagBaseFieldParser( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" pattern:"^[a-zA-Z0-9_]*$"`, + }}, + ).ComplementSchema(&schema) + assert.NoError(t, err) + assert.Equal(t, "^[a-zA-Z0-9_]*$", schema.Pattern) + }) + + t.Run("Pattern tag array", func(t *testing.T) { + t.Parallel() + + schema := spec.Schema{} + schema.Type = []string{"array"} + schema.Items = &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + } + err := newTagBaseFieldParser( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" pattern:"^[a-zA-Z0-9_]*$"`, + }}, + ).ComplementSchema(&schema) + assert.NoError(t, err) + assert.Equal(t, "^[a-zA-Z0-9_]*$", schema.Items.Schema.Pattern) + }) } diff --git a/field_parser_v3_test.go b/field_parser_v3_test.go new file mode 100644 index 000000000..da544b9b2 --- /dev/null +++ b/field_parser_v3_test.go @@ -0,0 +1,666 @@ +package swag + +import ( + "go/ast" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/sv-tools/openapi/spec" +) + +func TestDefaultFieldParserV3(t *testing.T) { + t.Run("Example tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" example:"one"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, "one", schema.Spec.Example) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" example:""`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, "", schema.Spec.Example) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"float"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" example:"one"`, + }}, + ).ComplementSchema(schema) + assert.Error(t, err) + }) + + t.Run("Format tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" format:"csv"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, "csv", schema.Spec.Format) + }) + + t.Run("Required tag", func(t *testing.T) { + t.Parallel() + + got, err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" binding:"required"`, + }}, + ).IsRequired() + assert.NoError(t, err) + assert.Equal(t, true, got) + + got, err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required"`, + }}, + ).IsRequired() + assert.NoError(t, err) + assert.Equal(t, true, got) + }) + + t.Run("Default required tag", func(t *testing.T) { + t.Parallel() + + got, err := newTagBaseFieldParserV3( + &Parser{ + RequiredByDefault: true, + }, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test"`, + }}, + ).IsRequired() + assert.NoError(t, err) + assert.True(t, got) + }) + + t.Run("Optional tag", func(t *testing.T) { + t.Parallel() + + got, err := newTagBaseFieldParserV3( + &Parser{ + RequiredByDefault: true, + }, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" binding:"optional"`, + }}, + ).IsRequired() + assert.NoError(t, err) + assert.False(t, got) + + got, err = newTagBaseFieldParserV3( + &Parser{ + RequiredByDefault: true, + }, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"optional"`, + }}, + ).IsRequired() + assert.NoError(t, err) + assert.False(t, got) + }) + + t.Run("Extensions tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"int"} + schema.Spec.Extensions = map[string]interface{}{} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" extensions:"x-nullable,x-abc=def,!x-omitempty,x-example=[0, 9],x-example2={çãíœ, (bar=(abc, def)), [0,9]}"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, true, schema.Spec.Extensions["x-nullable"]) + assert.Equal(t, "def", schema.Spec.Extensions["x-abc"]) + assert.Equal(t, false, schema.Spec.Extensions["x-omitempty"]) + assert.Equal(t, "[0, 9]", schema.Spec.Extensions["x-example"]) + assert.Equal(t, "{çãíœ, (bar=(abc, def)), [0,9]}", schema.Spec.Extensions["x-example2"]) + }) + + t.Run("Enums tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" enums:"a,b,c"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, []interface{}{"a", "b", "c"}, schema.Spec.Enum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"float"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" enums:"a,b,c"`, + }}, + ).ComplementSchema(schema) + assert.Error(t, err) + }) + + t.Run("EnumVarNames tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"int"} + schema.Spec.Extensions = map[string]interface{}{} + schema.Spec.Enum = []interface{}{} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" enums:"0,1,2" x-enum-varnames:"Daily,Weekly,Monthly"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, []interface{}{"Daily", "Weekly", "Monthly"}, schema.Spec.Extensions["x-enum-varnames"]) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"int"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" enums:"0,1,2,3" x-enum-varnames:"Daily,Weekly,Monthly"`, + }}, + ).ComplementSchema(schema) + assert.Error(t, err) + + // Test for an array of enums + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"array"} + schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + schema.Spec.Items.Schema.Spec.Type = []string{"int"} + + schema.Spec.Extensions = map[string]interface{}{} + schema.Spec.Enum = []interface{}{} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" enums:"0,1,2" x-enum-varnames:"Daily,Weekly,Monthly"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, []interface{}{"Daily", "Weekly", "Monthly"}, schema.Spec.Items.Schema.Spec.Extensions["x-enum-varnames"]) + assert.Equal(t, map[string]any{}, schema.Spec.Extensions) + }) + + t.Run("Default tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" default:"pass"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, "pass", schema.Spec.Default) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"float"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" default:"pass"`, + }}, + ).ComplementSchema(schema) + assert.Error(t, err) + }) + + t.Run("Numeric value", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"integer"} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" maximum:"1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + max := int(1) + assert.Equal(t, &max, schema.Spec.Maximum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"integer"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" maximum:"one"`, + }}, + ).ComplementSchema(schema) + assert.Error(t, err) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"number"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" maximum:"1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + max = int(1) + assert.Equal(t, &max, schema.Spec.Maximum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"number"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" maximum:"one"`, + }}, + ).ComplementSchema(schema) + assert.Error(t, err) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"number"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" multipleOf:"1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + multipleOf := int(1) + assert.Equal(t, &multipleOf, schema.Spec.MultipleOf) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"number"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" multipleOf:"one"`, + }}, + ).ComplementSchema(schema) + assert.Error(t, err) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"integer"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" minimum:"1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + min := int(1) + assert.Equal(t, &min, schema.Spec.Minimum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"integer"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" minimum:"one"`, + }}, + ).ComplementSchema(schema) + assert.Error(t, err) + }) + + t.Run("String value", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" maxLength:"1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + max := int(1) + assert.Equal(t, &max, schema.Spec.MaxLength) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" maxLength:"one"`, + }}, + ).ComplementSchema(schema) + assert.Error(t, err) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" minLength:"1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + min := int(1) + assert.Equal(t, &min, schema.Spec.MinLength) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" minLength:"one"`, + }}, + ).ComplementSchema(schema) + assert.Error(t, err) + }) + + t.Run("Readonly tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" readonly:"true"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, true, schema.Spec.ReadOnly) + }) +} + +func TestValidTagsV3(t *testing.T) { + t.Run("Required with max/min tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,max=10,min=1"`, + }}, + ).ComplementSchema(schema) + max := int(10) + min := int(1) + assert.NoError(t, err) + assert.Equal(t, &max, schema.Spec.MaxLength) + assert.Equal(t, &min, schema.Spec.MinLength) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,max=10,gte=1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, &max, schema.Spec.MaxLength) + assert.Equal(t, &min, schema.Spec.MinLength) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"integer"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,max=10,min=1"`, + }}, + ).ComplementSchema(schema) + maxFloat64 := int(10) + minFloat64 := int(1) + assert.NoError(t, err) + assert.Equal(t, &maxFloat64, schema.Spec.Maximum) + assert.Equal(t, &minFloat64, schema.Spec.Minimum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"array"} + + schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + schema.Spec.Items.Schema.Spec.Type = []string{"string"} + + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,max=10,min=1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, &max, schema.Spec.MaxItems) + assert.Equal(t, &min, schema.Spec.MinItems) + + // wrong validate tag will be ignored. + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,max=ten,min=1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Empty(t, schema.Spec.MaxItems) + assert.Equal(t, &min, schema.Spec.MinItems) + }) + t.Run("Required with oneof tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,oneof='red book' 'green book'"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, []interface{}{"red book", "green book"}, schema.Spec.Enum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"integer"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,oneof=1 2 3"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, []interface{}{1, 2, 3}, schema.Spec.Enum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"array"} + + schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + schema.Spec.Items.Schema.Spec.Type = []string{"string"} + + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,oneof=red green yellow"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, []interface{}{"red", "green", "yellow"}, schema.Spec.Items.Schema.Spec.Enum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,oneof='red green' blue 'c0x2Cc' 'd0x7Cd'"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, []interface{}{"red green", "blue", "c,c", "d|d"}, schema.Spec.Enum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,oneof='c0x9Ab' book"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, []interface{}{"c0x9Ab", "book"}, schema.Spec.Enum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" binding:"oneof=foo bar" validate:"required,oneof=foo bar" enums:"a,b,c"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, []interface{}{"a", "b", "c"}, schema.Spec.Enum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"string"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" binding:"oneof=aa bb" validate:"required,oneof=foo bar"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, []interface{}{"aa", "bb"}, schema.Spec.Enum) + }) + t.Run("Required with unique tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"array"} + schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + schema.Spec.Items.Schema.Spec.Type = []string{"string"} + + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,unique"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.True(t, *schema.Spec.UniqueItems) + }) + + t.Run("All tag", func(t *testing.T) { + t.Parallel() + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"array"} + schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + schema.Spec.Items.Schema.Spec.Type = []string{"string"} + + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,unique,max=10,min=1,oneof=a0x2Cc 'c0x7Cd book',omitempty,dive,max=1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.True(t, *schema.Spec.UniqueItems) + + max := int(10) + min := int(1) + assert.Equal(t, &max, schema.Spec.MaxItems) + assert.Equal(t, &min, schema.Spec.MinItems) + assert.Equal(t, []interface{}{"a,c", "c|d book"}, schema.Spec.Items.Schema.Spec.Enum) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"array"} + schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + schema.Spec.Items.Schema.Spec.Type = []string{"string"} + + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,oneof=,max=10=90,min=1"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Empty(t, schema.Spec.UniqueItems) + assert.Empty(t, schema.Spec.MaxItems) + assert.Equal(t, &min, schema.Spec.MinItems) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"array"} + schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + schema.Spec.Items.Schema.Spec.Type = []string{"string"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,max=10,min=one"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, &max, schema.Spec.MaxItems) + assert.Empty(t, schema.Spec.MinItems) + + schema = spec.NewSchemaSpec() + schema.Spec.Type = []string{"integer"} + err = newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"required,oneof=one two"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Empty(t, schema.Spec.Enum) + }) + + t.Run("Pattern tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{"array"} + schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) + schema.Spec.Items.Schema.Spec.Type = []string{"string"} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" pattern:"^[a-zA-Z0-9_]*$"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, "^[a-zA-Z0-9_]*$", schema.Spec.Items.Schema.Spec.Pattern) + }) + + t.Run("Pattern tag array", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = typeString + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" pattern:"^[a-zA-Z0-9_]*$"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Equal(t, "^[a-zA-Z0-9_]*$", schema.Spec.Pattern) + }) +} diff --git a/field_parserv3.go b/field_parserv3.go index b276a462e..733907f4b 100644 --- a/field_parserv3.go +++ b/field_parserv3.go @@ -25,6 +25,7 @@ type structFieldV3 struct { enums []interface{} enumVarNames []interface{} unique bool + pattern string } func (sf *structFieldV3) setOneOf(valValue string) { @@ -232,6 +233,11 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st if minLength != nil { field.minLength = minLength } + + pattern, ok := ps.tag.Lookup(patternTag) + if ok { + field.pattern = pattern + } } // json:"name,string" or json:",string" @@ -355,6 +361,7 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st elemSchema.MaxLength = field.maxLength elemSchema.MinLength = field.minLength elemSchema.Enum = field.enums + elemSchema.Pattern = field.pattern return nil } diff --git a/operation.go b/operation.go index 308dc30ff..7569ee790 100644 --- a/operation.go +++ b/operation.go @@ -422,6 +422,7 @@ const ( readOnlyTag = "readonly" extensionsTag = "extensions" collectionFormatTag = "collectionFormat" + patternTag = "pattern" ) var regexAttributes = map[string]*regexp.Regexp{ From ce8b32c56d73a24e1cab8d343c5bd8909bbcbd33 Mon Sep 17 00:00:00 2001 From: Leo Palmer Sunmo Date: Mon, 17 Apr 2023 18:54:28 +0200 Subject: [PATCH 16/34] Add option to set template delimiters (#1499) * Add template action delimiter cli flag * Add delims to generator config and template Also adds tests using the "quote" test as a base. This has to have a custom Instance name or it will clash with the "quotes" one and panic since it will have registered two "swagger" instances in the package test. * Add testdata for custom delim flags Based on the "quote" testdata. * Add delims to the spec, with tests. Make sure we don't add delims if they are empty. This shouldn't be possible, but might as well be safe. * Go mod tidy and sum update * Make the CLI experience a bit cleaner * Revert go.mod and sum * Update readme --- .gitignore | 2 + README.md | 13 ++++++ cmd/swag/main.go | 21 ++++++++++ gen/gen.go | 74 ++++++++++++++++++++++------------- gen/gen_test.go | 61 +++++++++++++++++++++++++++++ spec.go | 14 +++++-- spec_test.go | 35 +++++++++++++++++ testdata/delims/api/api.go | 11 ++++++ testdata/delims/expected.json | 39 ++++++++++++++++++ testdata/delims/main.go | 21 ++++++++++ 10 files changed, 260 insertions(+), 31 deletions(-) create mode 100644 testdata/delims/api/api.go create mode 100644 testdata/delims/expected.json create mode 100644 testdata/delims/main.go diff --git a/.gitignore b/.gitignore index 2438076f8..b24be87c2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ dist testdata/simple*/docs testdata/quotes/docs testdata/quotes/quotes.so +testdata/delims/docs +testdata/delims/delims.so example/basic/docs/* example/celler/docs/* cover.out diff --git a/README.md b/README.md index fb931fbbb..c9ef0effd 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ OPTIONS: --overridesFile value File to read global type overrides from. (default: ".swaggo") --parseGoList Parse dependency via 'go list' (default: true) --tags value, -t value A comma-separated list of tags to filter the APIs for which the documentation is generated.Special case if the tag is prefixed with the '!' character then the APIs with that tag will be excluded + --templateDelims value, --td value Provide custom delimeters for Go template generation. The format is leftDelim,rightDelim. For example: "[[,]]" --collectionFormat value, --cf value Set default collection format (default: "csv") --help, -h show help (default: false) ``` @@ -908,6 +909,18 @@ By default `swag` command generates Swagger specification in three different fil If you would like to limit a set of file types which should be generated you can use `--outputTypes` (short `-ot`) flag. Default value is `go,json,yaml` - output types separated with comma. To limit output only to `go` and `yaml` files, you would write `go,yaml`. With complete command that would be `swag init --outputTypes go,yaml`. +### Change the default Go Template action delimiters +[#980](https://github.com/swaggo/swag/issues/980) +[#1177](https://github.com/swaggo/swag/issues/1177) + +If your swagger annotations or struct fields contain "{{" or "}}", the template generation will most likely fail, as these are the default delimiters for [go templates](https://pkg.go.dev/text/template#Template.Delims). + +To make the generation work properly, you can change the default delimiters with `-td`. For example: +```console +swag init -g http/api.go -td "[[,]]" +``` +The new delimiter is a string with the format "``,``". + ## About the Project This project was inspired by [yvasiyarov/swagger](https://github.com/yvasiyarov/swagger) but we simplified the usage and added support a variety of [web frameworks](#supported-web-frameworks). Gopher image source is [tenntenn/gopher-stickers](https://github.com/tenntenn/gopher-stickers). It has licenses [creative commons licensing](http://creativecommons.org/licenses/by/3.0/deed.en). ## Contributors diff --git a/cmd/swag/main.go b/cmd/swag/main.go index 80c87cdb1..b4ad0834e 100644 --- a/cmd/swag/main.go +++ b/cmd/swag/main.go @@ -36,6 +36,7 @@ const ( tagsFlag = "tags" parseExtensionFlag = "parseExtension" openAPIVersionFlag = "v3.1" + templateDelimsFlag = "templateDelims" packageName = "packageName" collectionFormatFlag = "collectionFormat" ) @@ -149,6 +150,12 @@ var initFlags = []cli.Flag{ Value: false, Usage: "Generate OpenAPI V3.1 spec", }, + &cli.StringFlag{ + Name: templateDelimsFlag, + Aliases: []string{"td"}, + Value: "", + Usage: "Provide custom delimeters for Go template generation. The format is leftDelim,rightDelim. For example: \"[[,]]\"", + }, &cli.StringFlag{ Name: packageName, Value: "", @@ -171,6 +178,18 @@ func initAction(ctx *cli.Context) error { return fmt.Errorf("not supported %s propertyStrategy", strategy) } + leftDelim, rightDelim := "{{", "}}" + + if ctx.IsSet(templateDelimsFlag) { + delims := strings.Split(ctx.String(templateDelimsFlag), ",") + if len(delims) != 2 { + return fmt.Errorf("exactly two template delimeters must be provided, comma separated") + } else if delims[0] == delims[1] { + return fmt.Errorf("template delimiters must be different") + } + leftDelim, rightDelim = strings.TrimSpace(delims[0]), strings.TrimSpace(delims[1]) + } + outputTypes := strings.Split(ctx.String(outputTypesFlag), ",") if len(outputTypes) == 0 { return fmt.Errorf("no output types specified") @@ -205,6 +224,8 @@ func initAction(ctx *cli.Context) error { OverridesFile: ctx.String(overridesFileFlag), ParseGoList: ctx.Bool(parseGoListFlag), Tags: ctx.String(tagsFlag), + LeftTemplateDelim: leftDelim, + RightTemplateDelim: rightDelim, PackageName: ctx.String(packageName), Debugger: logger, GenerateOpenAPI3Doc: ctx.Bool(openAPIVersionFlag), diff --git a/gen/gen.go b/gen/gen.go index aa3998d86..82e2e0d9c 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -135,6 +135,12 @@ type Config struct { // include only tags mentioned when searching, comma separated Tags string + // LeftTemplateDelim defines the left delimiter for the template generation + LeftTemplateDelim string + + // RightTemplateDelim defines the right delimiter for the template generation + RightTemplateDelim string + // GenerateOpenAPI3Doc if true, OpenAPI V3.1 spec will be generated GenerateOpenAPI3Doc bool @@ -161,6 +167,14 @@ func (g *Gen) Build(config *Config) error { } } + if config.LeftTemplateDelim == "" { + config.LeftTemplateDelim = "{{" + } + + if config.RightTemplateDelim == "" { + config.RightTemplateDelim = "}}" + } + var overrides map[string]string if config.OverridesFile != "" { @@ -403,7 +417,7 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *v2.Swagg generator, err := template.New("oas2.tmpl").Funcs(template.FuncMap{ "printDoc": func(v string) string { // Add schemes - v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:] + v = "{\n \"schemes\": " + config.LeftTemplateDelim + " marshal .Schemes " + config.RightTemplateDelim + "," + v[1:] // Sanitize backticks return strings.Replace(v, "`", "`+\"`\"+`", -1) }, @@ -422,16 +436,16 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *v2.Swagg Info: &v2.Info{ VendorExtensible: swagger.Info.VendorExtensible, InfoProps: v2.InfoProps{ - Description: "{{escape .Description}}", - Title: "{{.Title}}", + Description: config.LeftTemplateDelim + "escape .Description" + config.RightTemplateDelim, + Title: config.LeftTemplateDelim + ".Title" + config.RightTemplateDelim, TermsOfService: swagger.Info.TermsOfService, Contact: swagger.Info.Contact, License: swagger.Info.License, - Version: "{{.Version}}", + Version: config.LeftTemplateDelim + ".Version" + config.RightTemplateDelim, }, }, - Host: "{{.Host}}", - BasePath: "{{.BasePath}}", + Host: config.LeftTemplateDelim + ".Host" + config.RightTemplateDelim, + BasePath: config.LeftTemplateDelim + ".BasePath" + config.RightTemplateDelim, Paths: swagger.Paths, Definitions: swagger.Definitions, Parameters: swagger.Parameters, @@ -452,29 +466,33 @@ func (g *Gen) writeGoDoc(packageName string, output io.Writer, swagger *v2.Swagg buffer := &bytes.Buffer{} err = generator.Execute(buffer, struct { - Timestamp time.Time - Doc string - Host string - PackageName string - BasePath string - Title string - Description string - Version string - InstanceName string - Schemes []string - GeneratedTime bool + Timestamp time.Time + Doc string + Host string + PackageName string + BasePath string + Title string + Description string + Version string + InstanceName string + Schemes []string + GeneratedTime bool + LeftTemplateDelim string + RightTemplateDelim string }{ - Timestamp: time.Now(), - GeneratedTime: config.GeneratedTime, - Doc: string(buf), - Host: swagger.Host, - PackageName: packageName, - BasePath: swagger.BasePath, - Schemes: swagger.Schemes, - Title: swagger.Info.Title, - Description: swagger.Info.Description, - Version: swagger.Info.Version, - InstanceName: config.InstanceName, + Timestamp: time.Now(), + GeneratedTime: config.GeneratedTime, + Doc: string(buf), + Host: swagger.Host, + PackageName: packageName, + BasePath: swagger.BasePath, + Schemes: swagger.Schemes, + Title: swagger.Info.Title, + Description: swagger.Info.Description, + Version: swagger.Info.Version, + InstanceName: config.InstanceName, + LeftTemplateDelim: config.LeftTemplateDelim, + RightTemplateDelim: config.RightTemplateDelim, }) if err != nil { return err diff --git a/gen/gen_test.go b/gen/gen_test.go index 661b67372..95ed3fc71 100644 --- a/gen/gen_test.go +++ b/gen/gen_test.go @@ -259,6 +259,67 @@ func TestGen_BuildDescriptionWithQuotes(t *testing.T) { assert.JSONEq(t, string(expectedJSON), jsonOutput) } +func TestGen_BuildDocCustomDelims(t *testing.T) { + config := &Config{ + SearchDir: "../testdata/delims", + MainAPIFile: "./main.go", + OutputDir: "../testdata/delims/docs", + OutputTypes: outputTypes, + MarkdownFilesDir: "../testdata/delims", + InstanceName: "CustomDelims", + LeftTemplateDelim: "{%", + RightTemplateDelim: "%}", + } + + require.NoError(t, New().Build(config)) + + expectedFiles := []string{ + filepath.Join(config.OutputDir, "CustomDelims_docs.go"), + filepath.Join(config.OutputDir, "CustomDelims_swagger.json"), + filepath.Join(config.OutputDir, "CustomDelims_swagger.yaml"), + } + for _, expectedFile := range expectedFiles { + if _, err := os.Stat(expectedFile); os.IsNotExist(err) { + require.NoError(t, err) + } + } + + cmd := exec.Command("go", "build", "-buildmode=plugin", "github.com/swaggo/swag/testdata/delims") + + cmd.Dir = config.SearchDir + + output, err := cmd.CombinedOutput() + if err != nil { + require.NoError(t, err, string(output)) + } + + p, err := plugin.Open(filepath.Join(config.SearchDir, "delims.so")) + if err != nil { + require.NoError(t, err) + } + + defer os.Remove("delims.so") + + readDoc, err := p.Lookup("ReadDoc") + if err != nil { + require.NoError(t, err) + } + + jsonOutput := readDoc.(func() string)() + + var jsonDoc interface{} + if err := json.Unmarshal([]byte(jsonOutput), &jsonDoc); err != nil { + require.NoError(t, err) + } + + expectedJSON, err := os.ReadFile(filepath.Join(config.SearchDir, "expected.json")) + if err != nil { + require.NoError(t, err) + } + + assert.JSONEq(t, string(expectedJSON), jsonOutput) +} + func TestGen_jsonIndent(t *testing.T) { config := &Config{ SearchDir: searchDir, diff --git a/spec.go b/spec.go index 3a727c940..c18a365b0 100644 --- a/spec.go +++ b/spec.go @@ -17,13 +17,15 @@ type Spec struct { Description string InfoInstanceName string SwaggerTemplate string + LeftDelim string + RightDelim string } // ReadDoc parses SwaggerTemplate into swagger document. func (i *Spec) ReadDoc() string { i.Description = strings.ReplaceAll(i.Description, "\n", "\\n") - tpl, err := template.New("swagger_info").Funcs(template.FuncMap{ + tpl := template.New("swagger_info").Funcs(template.FuncMap{ "marshal": func(v interface{}) string { a, _ := json.Marshal(v) @@ -37,13 +39,19 @@ func (i *Spec) ReadDoc() string { return strings.ReplaceAll(str, "\\\\\"", "\\\\\\\"") }, - }).Parse(i.SwaggerTemplate) + }) + + if i.LeftDelim != "" && i.RightDelim != "" { + tpl = tpl.Delims(i.LeftDelim, i.RightDelim) + } + + parsed, err := tpl.Parse(i.SwaggerTemplate) if err != nil { return i.SwaggerTemplate } var doc bytes.Buffer - if err = tpl.Execute(&doc, i); err != nil { + if err = parsed.Execute(&doc, i); err != nil { return i.SwaggerTemplate } diff --git a/spec_test.go b/spec_test.go index 75eb7f37d..f20d70ce3 100644 --- a/spec_test.go +++ b/spec_test.go @@ -63,6 +63,8 @@ func TestSpec_ReadDoc(t *testing.T) { Description string InfoInstanceName string SwaggerTemplate string + LeftDelim string + RightDelim string } tests := []struct { @@ -132,6 +134,37 @@ func TestSpec_ReadDoc(t *testing.T) { }, want: "{{ .Schemesa }}", }, + { + name: "TestReadDocCustomDelims", + fields: fields{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/", + InfoInstanceName: "TestInstanceName", + SwaggerTemplate: `{ + "swagger": "2.0", + "info": { + "description": "{%escape .Description%}", + "title": "{%.Title%}", + "version": "{%.Version%}" + }, + "host": "{%.Host%}", + "basePath": "{%.BasePath%}", + }`, + LeftDelim: "{%", + RightDelim: "%}", + }, + want: "{" + + "\n\t\t\t\"swagger\": \"2.0\"," + + "\n\t\t\t\"info\": {" + + "\n\t\t\t\t\"description\": \"\",\n\t\t\t\t\"" + + "title\": \"\"," + + "\n\t\t\t\t\"version\": \"1.0\"" + + "\n\t\t\t}," + + "\n\t\t\t\"host\": \"localhost:8080\"," + + "\n\t\t\t\"basePath\": \"/\"," + + "\n\t\t}", + }, } for _, tt := range tests { @@ -145,6 +178,8 @@ func TestSpec_ReadDoc(t *testing.T) { Description: tt.fields.Description, InfoInstanceName: tt.fields.InfoInstanceName, SwaggerTemplate: tt.fields.SwaggerTemplate, + LeftDelim: tt.fields.LeftDelim, + RightDelim: tt.fields.RightDelim, } assert.Equal(t, tt.want, doc.ReadDoc()) diff --git a/testdata/delims/api/api.go b/testdata/delims/api/api.go new file mode 100644 index 000000000..b2081a76b --- /dev/null +++ b/testdata/delims/api/api.go @@ -0,0 +1,11 @@ +package api + +// MyFunc godoc +// @Description My Function +// @Success 200 {object} MyStruct +// @Router /myfunc [get] +func MyFunc() {} + +type MyStruct struct { + URLTemplate string `json:"urltemplate" example:"http://example.org/{{ path }}" swaggertype:"string"` +} diff --git a/testdata/delims/expected.json b/testdata/delims/expected.json new file mode 100644 index 000000000..423993243 --- /dev/null +++ b/testdata/delims/expected.json @@ -0,0 +1,39 @@ +{ + "schemes": [], + "swagger": "2.0", + "info": { + "description": "Testing custom template delimeters", + "title": "Swagger Example API", + "termsOfService": "http://swagger.io/terms/", + "contact": {}, + "version": "1.0" + }, + "host": "", + "basePath": "", + "paths": { + "/myfunc": { + "get": { + "description": "My Function", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.MyStruct" + } + } + } + } + } + }, + "definitions": { + "api.MyStruct": { + "type": "object", + "properties": { + "urltemplate": { + "type": "string", + "example": "http://example.org/{{ path }}" + } + } + } + } +} \ No newline at end of file diff --git a/testdata/delims/main.go b/testdata/delims/main.go new file mode 100644 index 000000000..cf5268cad --- /dev/null +++ b/testdata/delims/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/swaggo/swag" + "github.com/swaggo/swag/testdata/delims/api" + _ "github.com/swaggo/swag/testdata/delims/docs" +) + +func ReadDoc() string { + doc, _ := swag.ReadDoc("CustomDelims") + return doc +} + +// @title Swagger Example API +// @version 1.0 +// @description Testing custom template delimeters +// @termsOfService http://swagger.io/terms/ + +func main() { + api.MyFunc() +} From c85d570f62aed52fa946cef3b676effc8e9b77ff Mon Sep 17 00:00:00 2001 From: sdghchj Date: Tue, 18 Apr 2023 00:58:42 +0800 Subject: [PATCH 17/34] fix bug: enums of explicit type conversion (#1556) Signed-off-by: sdghchj --- package.go | 5 ++--- testdata/enums/expected.json | 15 ++++++++++----- testdata/enums/types/model.go | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/package.go b/package.go index 08f8b92b3..bc25786ce 100644 --- a/package.go +++ b/package.go @@ -158,19 +158,18 @@ func (pkg *PackageDefinitions) evaluateConstValue(file *ast.File, iota int, expr if name == "uintptr" { name = "uint" } + value, _ := pkg.evaluateConstValue(file, iota, arg, globalEvaluator, recursiveStack) if IsGolangPrimitiveType(name) { - value, _ := pkg.evaluateConstValue(file, iota, arg, globalEvaluator, recursiveStack) value = EvaluateDataConversion(value, name) return value, nil } else if name == "len" { - value, _ := pkg.evaluateConstValue(file, iota, arg, globalEvaluator, recursiveStack) return reflect.ValueOf(value).Len(), nil } typeDef := globalEvaluator.FindTypeSpec(name, file) if typeDef == nil { return nil, nil } - return arg, valueExpr.Fun + return value, valueExpr.Fun } else if selector, ok := valueExpr.Fun.(*ast.SelectorExpr); ok { typeDef := globalEvaluator.FindTypeSpec(fullTypeName(selector.X.(*ast.Ident).Name, selector.Sel.Name), file) if typeDef == nil { diff --git a/testdata/enums/expected.json b/testdata/enums/expected.json index 9f4146802..79b56f8ad 100644 --- a/testdata/enums/expected.json +++ b/testdata/enums/expected.json @@ -118,7 +118,8 @@ 1, 2, 4, - 8 + 8, + 3 ], "type": "integer", "x-enum-comments": { @@ -131,7 +132,8 @@ "Mask1", "Mask2", "Mask3", - "Mask4" + "Mask4", + "Mask5" ], "name": "mask", "in": "formData" @@ -197,7 +199,8 @@ 1, 2, 4, - 8 + 8, + 3 ], "type": "integer" }, @@ -268,7 +271,8 @@ 1, 2, 4, - 8 + 8, + 3 ], "x-enum-comments": { "Mask1": "Mask1", @@ -280,7 +284,8 @@ "Mask1", "Mask2", "Mask3", - "Mask4" + "Mask4", + "Mask5" ] }, "types.Person": { diff --git a/testdata/enums/types/model.go b/testdata/enums/types/model.go index 47822811e..a19fe4990 100644 --- a/testdata/enums/types/model.go +++ b/testdata/enums/types/model.go @@ -30,6 +30,7 @@ const ( Mask2 /* Mask2 */ Mask3 // Mask3 Mask4 // Mask4 + Mask5 = Mask(A + B) ) type Type string From e3bb142f0e08a4208115f5e75d01a55cfae11b7c Mon Sep 17 00:00:00 2001 From: Nerzal Date: Wed, 5 Apr 2023 18:19:12 +0200 Subject: [PATCH 18/34] Implementing OpenAPI 3.1.0 spec (#1513) * fix x-tagGroups * fix module name * change paths * refactoring * update dependencies * stuff * add log * fix finding of main file * fix broken type resolution * fix bug * clean deps * fix tool after merging upstream * use json-iterator to marshal json * fix generating of json examples * update config used by jsoniter * bump version * update dependencies * resolve merge conflicts * use newest go in docker * yep * fix gen * fix gen * update swag version * yep * fix parser * fix some tests * fix all tests * parse most of general api description * implement security scheme parsing * parse oauth2 specs * parse scopes and extensions in security schemes * extend parsing security stuff * process v3 routes * meh * find unimported types * parse basic operation info * parse primitive and object parameters * generate openapi spec * fix module name * cleanup * update version to 2.0 * fix issues that appread after merging * cleanup after merge conflicts * fix all tests * add go 1.19 to workflows * pin dockerfile to 1.19.7 * Set minimum supported Go version to 1.18.x * parse response headers * copy readme * started to implement field parser * Refactor: use RefOrSpec instead of Spec * start to add tests for operationv3 * fix tests * implement allOf with primitive types * Add NestedPrimitiveArrayType test * implement TestParseResponseCommentWithNestedFieldsV3 * add more tests * parse arrays and maps * fix implementation of map types * implement more tests * fix example docs * adjust example * fix example jsons * support array types in Parameters * implement more tests, implement correct collectionFormat handling * finish implementation of operationv3 tests * all tests green * fix parsing of security definitions * add test for generalAPI info * end of day checkin * Update example.json * fix codeSamples from file and fix creation of operations * fix resolving of schema ref errors * fix tests that broke due to fixes on model parsing * Fix creating schemes of array types of custom objects * Fix resolution of refSchemas * cleanup * update dependencies * cleanup * Update README.md reset readme.md * Update README_zh-CN.md reset readme_zh-CN * update dependency * reset test file --------- Co-authored-by: Tobias Theel --- field_parser.go | 1 + gen/genv3.go | 202 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- 3 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 gen/genv3.go diff --git a/field_parser.go b/field_parser.go index f7d3fed65..de523416a 100644 --- a/field_parser.go +++ b/field_parser.go @@ -21,6 +21,7 @@ const ( swaggerTypeTag = "swaggertype" swaggerIgnoreTag = "swaggerignore" ) +var _ FieldParser = &tagBaseFieldParser{} var _ FieldParser = &tagBaseFieldParser{} diff --git a/gen/genv3.go b/gen/genv3.go new file mode 100644 index 000000000..8010660f2 --- /dev/null +++ b/gen/genv3.go @@ -0,0 +1,202 @@ +package gen + +import ( + "bytes" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + "text/template" + "time" + + "github.com/sv-tools/openapi/spec" + "github.com/swaggo/swag" +) + +type openAPITypeWriter func(*Config, *spec.OpenAPI) error + +func (g *Gen) writeDocOpenAPI(config *Config, openAPI *spec.OpenAPI) error { + var filename = "docs.go" + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + docFileName := path.Join(config.OutputDir, filename) + + absOutputDir, err := filepath.Abs(config.OutputDir) + if err != nil { + return err + } + + packageName := filepath.Base(absOutputDir) + + docs, err := os.Create(docFileName) + if err != nil { + return err + } + defer docs.Close() + + // Write doc + err = g.writeGoDocV3(packageName, docs, openAPI, config) + if err != nil { + return err + } + + g.debug.Printf("create docs.go at %+v", docFileName) + + return nil +} + +func (g *Gen) writeJSONOpenAPI(config *Config, swagger *spec.OpenAPI) error { + var filename = "swagger.json" + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + jsonFileName := path.Join(config.OutputDir, filename) + + b, err := g.jsonIndent(swagger) + if err != nil { + return err + } + + err = g.writeFile(b, jsonFileName) + if err != nil { + return err + } + + g.debug.Printf("create swagger.json at %+v", jsonFileName) + + return nil +} + +func (g *Gen) writeYAMLOpenAPI(config *Config, swagger *spec.OpenAPI) error { + var filename = "swagger.yaml" + + if config.InstanceName != swag.Name { + filename = config.InstanceName + "_" + filename + } + + yamlFileName := path.Join(config.OutputDir, filename) + + b, err := g.json(swagger) + if err != nil { + return err + } + + y, err := g.jsonToYAML(b) + if err != nil { + return fmt.Errorf("cannot covert json to yaml error: %s", err) + } + + err = g.writeFile(y, yamlFileName) + if err != nil { + return err + } + + g.debug.Printf("create swagger.yaml at %+v", yamlFileName) + + return nil +} + +func (g *Gen) writeGoDocV3(packageName string, output io.Writer, openAPI *spec.OpenAPI, config *Config) error { + generator, err := template.New("swagger_info").Funcs(template.FuncMap{ + "printDoc": func(v string) string { + // Add schemes + v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:] + // Sanitize backticks + return strings.Replace(v, "`", "`+\"`\"+`", -1) + }, + }).Parse(packageTemplateV3) + if err != nil { + return err + } + + openAPISpec := spec.OpenAPI{ + Components: openAPI.Components, + OpenAPI: openAPI.OpenAPI, + Info: &spec.Extendable[spec.Info]{ + Spec: &spec.Info{ + Description: "{{escape .Description}}", + Title: "{{.Title}}", + Version: "{{.Version}}", + TermsOfService: openAPI.Info.Spec.TermsOfService, + Contact: openAPI.Info.Spec.Contact, + License: openAPI.Info.Spec.License, + Summary: openAPI.Info.Spec.Summary, + }, + Extensions: openAPI.Info.Extensions, + }, + ExternalDocs: openAPI.ExternalDocs, + Paths: openAPI.Paths, + WebHooks: openAPI.WebHooks, + JsonSchemaDialect: openAPI.JsonSchemaDialect, + Security: openAPI.Security, + Tags: openAPI.Tags, + Servers: openAPI.Servers, + } + + // crafted docs.json + buf, err := g.jsonIndent(openAPISpec) + if err != nil { + return err + } + + buffer := &bytes.Buffer{} + + err = generator.Execute(buffer, struct { + Timestamp time.Time + Doc string + PackageName string + Title string + Description string + Version string + InstanceName string + GeneratedTime bool + }{ + Timestamp: time.Now(), + GeneratedTime: config.GeneratedTime, + Doc: string(buf), + PackageName: packageName, + Title: openAPI.Info.Spec.Title, + Description: openAPI.Info.Spec.Description, + Version: openAPI.Info.Spec.Version, + InstanceName: config.InstanceName, + }) + if err != nil { + return err + } + + code := g.formatSource(buffer.Bytes()) + + // write + _, err = output.Write(code) + + return err +} + +var packageTemplateV3 = `// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT + +package docs + +import "github.com/swaggo/swag" + +const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = ` + "`{{ printDoc .Doc}}`" + ` + +// SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} holds exported Swagger Info so clients can modify it +var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = &swag.Spec{ + Version: {{ printf "%q" .Version}}, + Title: {{ printf "%q" .Title}}, + Description: {{ printf "%q" .Description}}, + InfoInstanceName: {{ printf "%q" .InstanceName }}, + SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, +} + +func init() { + swag.Register(SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}) +} +` diff --git a/go.mod b/go.mod index 32f79d171..5a6271167 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/swaggo/swag/v2 +module github.com/swaggo/swag go 1.18 From d76b289512c83b2135d5cff0094ec205340da806 Mon Sep 17 00:00:00 2001 From: Nerzal Date: Wed, 5 Apr 2023 18:19:12 +0200 Subject: [PATCH 19/34] Implementing OpenAPI 3.1.0 spec (#1513) * fix x-tagGroups * fix module name * change paths * refactoring * update dependencies * stuff * add log * fix finding of main file * fix broken type resolution * fix bug * clean deps * fix tool after merging upstream * use json-iterator to marshal json * fix generating of json examples * update config used by jsoniter * bump version * update dependencies * resolve merge conflicts * use newest go in docker * yep * fix gen * fix gen * update swag version * yep * fix parser * fix some tests * fix all tests * parse most of general api description * implement security scheme parsing * parse oauth2 specs * parse scopes and extensions in security schemes * extend parsing security stuff * process v3 routes * meh * find unimported types * parse basic operation info * parse primitive and object parameters * generate openapi spec * fix module name * cleanup * update version to 2.0 * fix issues that appread after merging * cleanup after merge conflicts * fix all tests * add go 1.19 to workflows * pin dockerfile to 1.19.7 * Set minimum supported Go version to 1.18.x * parse response headers * copy readme * started to implement field parser * Refactor: use RefOrSpec instead of Spec * start to add tests for operationv3 * fix tests * implement allOf with primitive types * Add NestedPrimitiveArrayType test * implement TestParseResponseCommentWithNestedFieldsV3 * add more tests * parse arrays and maps * fix implementation of map types * implement more tests * fix example docs * adjust example * fix example jsons * support array types in Parameters * implement more tests, implement correct collectionFormat handling * finish implementation of operationv3 tests * all tests green * fix parsing of security definitions * add test for generalAPI info * end of day checkin * Update example.json * fix codeSamples from file and fix creation of operations * fix resolving of schema ref errors * fix tests that broke due to fixes on model parsing * Fix creating schemes of array types of custom objects * Fix resolution of refSchemas * cleanup * update dependencies * cleanup * Update README.md reset readme.md * Update README_zh-CN.md reset readme_zh-CN * update dependency * reset test file --------- Co-authored-by: Tobias Theel --- cmd/swag/main.go | 2 +- field_parserv3.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/swag/main.go b/cmd/swag/main.go index b4ad0834e..57a2f460f 100644 --- a/cmd/swag/main.go +++ b/cmd/swag/main.go @@ -35,8 +35,8 @@ const ( quietFlag = "quiet" tagsFlag = "tags" parseExtensionFlag = "parseExtension" - openAPIVersionFlag = "v3.1" templateDelimsFlag = "templateDelims" + openAPIVersionFlag = "v3.1" packageName = "packageName" collectionFormatFlag = "collectionFormat" ) diff --git a/field_parserv3.go b/field_parserv3.go index 733907f4b..e5fdcea4c 100644 --- a/field_parserv3.go +++ b/field_parserv3.go @@ -464,13 +464,13 @@ func (sf *structFieldV3) parseValidTags(validTag string) { } } -func (sf *structFieldV3) parseEnumTags(enumTag string) error { - enumType := sf.schemaType - if sf.schemaType == ARRAY { - enumType = sf.arrayType +func (field *structFieldV3) parseEnumTags(enumTag string) error { + enumType := field.schemaType + if field.schemaType == ARRAY { + enumType = field.arrayType } - sf.enums = nil + field.enums = nil for _, e := range strings.Split(enumTag, ",") { value, err := defineType(enumType, e) @@ -478,7 +478,7 @@ func (sf *structFieldV3) parseEnumTags(enumTag string) error { return err } - sf.enums = append(sf.enums, value) + field.enums = append(field.enums, value) } return nil From 17cc74380abbb4821ee577e2562e67d51fbd2df2 Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Wed, 5 Apr 2023 22:28:30 +0300 Subject: [PATCH 20/34] chore: linting code (#1533) * chore: make fmt * chore: make lint --- field_parser.go | 1 + field_parserv3.go | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/field_parser.go b/field_parser.go index de523416a..988d09105 100644 --- a/field_parser.go +++ b/field_parser.go @@ -21,6 +21,7 @@ const ( swaggerTypeTag = "swaggertype" swaggerIgnoreTag = "swaggerignore" ) + var _ FieldParser = &tagBaseFieldParser{} var _ FieldParser = &tagBaseFieldParser{} diff --git a/field_parserv3.go b/field_parserv3.go index e5fdcea4c..733907f4b 100644 --- a/field_parserv3.go +++ b/field_parserv3.go @@ -464,13 +464,13 @@ func (sf *structFieldV3) parseValidTags(validTag string) { } } -func (field *structFieldV3) parseEnumTags(enumTag string) error { - enumType := field.schemaType - if field.schemaType == ARRAY { - enumType = field.arrayType +func (sf *structFieldV3) parseEnumTags(enumTag string) error { + enumType := sf.schemaType + if sf.schemaType == ARRAY { + enumType = sf.arrayType } - field.enums = nil + sf.enums = nil for _, e := range strings.Split(enumTag, ",") { value, err := defineType(enumType, e) @@ -478,7 +478,7 @@ func (field *structFieldV3) parseEnumTags(enumTag string) error { return err } - field.enums = append(field.enums, value) + sf.enums = append(sf.enums, value) } return nil From 6d1a872b18aabde30adca67a64331975bec60dcd Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Thu, 6 Apr 2023 02:36:27 +0300 Subject: [PATCH 21/34] chore: refactor code (#1539) * chore: refactor code * chore: lint * chore: lint * chore: use embed for generated doc --- gen/genv3.go | 202 --------------------------------------------------- 1 file changed, 202 deletions(-) delete mode 100644 gen/genv3.go diff --git a/gen/genv3.go b/gen/genv3.go deleted file mode 100644 index 8010660f2..000000000 --- a/gen/genv3.go +++ /dev/null @@ -1,202 +0,0 @@ -package gen - -import ( - "bytes" - "fmt" - "io" - "os" - "path" - "path/filepath" - "strings" - "text/template" - "time" - - "github.com/sv-tools/openapi/spec" - "github.com/swaggo/swag" -) - -type openAPITypeWriter func(*Config, *spec.OpenAPI) error - -func (g *Gen) writeDocOpenAPI(config *Config, openAPI *spec.OpenAPI) error { - var filename = "docs.go" - - if config.InstanceName != swag.Name { - filename = config.InstanceName + "_" + filename - } - - docFileName := path.Join(config.OutputDir, filename) - - absOutputDir, err := filepath.Abs(config.OutputDir) - if err != nil { - return err - } - - packageName := filepath.Base(absOutputDir) - - docs, err := os.Create(docFileName) - if err != nil { - return err - } - defer docs.Close() - - // Write doc - err = g.writeGoDocV3(packageName, docs, openAPI, config) - if err != nil { - return err - } - - g.debug.Printf("create docs.go at %+v", docFileName) - - return nil -} - -func (g *Gen) writeJSONOpenAPI(config *Config, swagger *spec.OpenAPI) error { - var filename = "swagger.json" - - if config.InstanceName != swag.Name { - filename = config.InstanceName + "_" + filename - } - - jsonFileName := path.Join(config.OutputDir, filename) - - b, err := g.jsonIndent(swagger) - if err != nil { - return err - } - - err = g.writeFile(b, jsonFileName) - if err != nil { - return err - } - - g.debug.Printf("create swagger.json at %+v", jsonFileName) - - return nil -} - -func (g *Gen) writeYAMLOpenAPI(config *Config, swagger *spec.OpenAPI) error { - var filename = "swagger.yaml" - - if config.InstanceName != swag.Name { - filename = config.InstanceName + "_" + filename - } - - yamlFileName := path.Join(config.OutputDir, filename) - - b, err := g.json(swagger) - if err != nil { - return err - } - - y, err := g.jsonToYAML(b) - if err != nil { - return fmt.Errorf("cannot covert json to yaml error: %s", err) - } - - err = g.writeFile(y, yamlFileName) - if err != nil { - return err - } - - g.debug.Printf("create swagger.yaml at %+v", yamlFileName) - - return nil -} - -func (g *Gen) writeGoDocV3(packageName string, output io.Writer, openAPI *spec.OpenAPI, config *Config) error { - generator, err := template.New("swagger_info").Funcs(template.FuncMap{ - "printDoc": func(v string) string { - // Add schemes - v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:] - // Sanitize backticks - return strings.Replace(v, "`", "`+\"`\"+`", -1) - }, - }).Parse(packageTemplateV3) - if err != nil { - return err - } - - openAPISpec := spec.OpenAPI{ - Components: openAPI.Components, - OpenAPI: openAPI.OpenAPI, - Info: &spec.Extendable[spec.Info]{ - Spec: &spec.Info{ - Description: "{{escape .Description}}", - Title: "{{.Title}}", - Version: "{{.Version}}", - TermsOfService: openAPI.Info.Spec.TermsOfService, - Contact: openAPI.Info.Spec.Contact, - License: openAPI.Info.Spec.License, - Summary: openAPI.Info.Spec.Summary, - }, - Extensions: openAPI.Info.Extensions, - }, - ExternalDocs: openAPI.ExternalDocs, - Paths: openAPI.Paths, - WebHooks: openAPI.WebHooks, - JsonSchemaDialect: openAPI.JsonSchemaDialect, - Security: openAPI.Security, - Tags: openAPI.Tags, - Servers: openAPI.Servers, - } - - // crafted docs.json - buf, err := g.jsonIndent(openAPISpec) - if err != nil { - return err - } - - buffer := &bytes.Buffer{} - - err = generator.Execute(buffer, struct { - Timestamp time.Time - Doc string - PackageName string - Title string - Description string - Version string - InstanceName string - GeneratedTime bool - }{ - Timestamp: time.Now(), - GeneratedTime: config.GeneratedTime, - Doc: string(buf), - PackageName: packageName, - Title: openAPI.Info.Spec.Title, - Description: openAPI.Info.Spec.Description, - Version: openAPI.Info.Spec.Version, - InstanceName: config.InstanceName, - }) - if err != nil { - return err - } - - code := g.formatSource(buffer.Bytes()) - - // write - _, err = output.Write(code) - - return err -} - -var packageTemplateV3 = `// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT - -package docs - -import "github.com/swaggo/swag" - -const docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = ` + "`{{ printDoc .Doc}}`" + ` - -// SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} holds exported Swagger Info so clients can modify it -var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} = &swag.Spec{ - Version: {{ printf "%q" .Version}}, - Title: {{ printf "%q" .Title}}, - Description: {{ printf "%q" .Description}}, - InfoInstanceName: {{ printf "%q" .InstanceName }}, - SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, -} - -func init() { - swag.Register(SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}.InstanceName(), SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}) -} -` From acb050860af539581eacc4f0c4945c292b378950 Mon Sep 17 00:00:00 2001 From: Nerzal Date: Mon, 17 Apr 2023 20:58:12 +0200 Subject: [PATCH 22/34] V2: Fix tests after module upgrade (#1557) * V2: Fix tests after v2 module upgrade * formData is now being placed into the request body instead of the parameters --------- Co-authored-by: Tobias Theel --- enums_test.go | 2 +- gen/gen_test.go | 2 +- operationv3.go | 14 +++--- operationv3_test.go | 42 +++++++--------- parser_test.go | 6 +-- parserv3_test.go | 5 +- testdata/conflict_name/expected.json | 16 +++--- .../internal/expected.json | 50 +++++++++---------- testdata/v3/simple/api/api.go | 6 +++ testdata/v3/simple/main.go | 1 + testdata/v3/simple/web/handler.go | 13 +++++ 11 files changed, 89 insertions(+), 68 deletions(-) diff --git a/enums_test.go b/enums_test.go index 1d8e1930f..f730dd647 100644 --- a/enums_test.go +++ b/enums_test.go @@ -14,7 +14,7 @@ func TestParseGlobalEnums(t *testing.T) { err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) require.NoError(t, err) - const constsPath = "github.com/swaggo/swag/testdata/enums/consts" + const constsPath = "github.com/swaggo/swag/v2/testdata/enums/consts" table := p.packages.packages[constsPath].ConstTable require.NotNil(t, table, "const table must not be nil") diff --git a/gen/gen_test.go b/gen/gen_test.go index 95ed3fc71..aade104f8 100644 --- a/gen/gen_test.go +++ b/gen/gen_test.go @@ -223,7 +223,7 @@ func TestGen_BuildDescriptionWithQuotes(t *testing.T) { require.NoError(t, err) } } - cmd := exec.Command("go", "build", "-buildmode=plugin", "github.com/swaggo/swag/testdata/quotes") + cmd := exec.Command("go", "build", "-buildmode=plugin", "github.com/swaggo/swag/v2/testdata/quotes") cmd.Dir = config.SearchDir diff --git a/operationv3.go b/operationv3.go index 393de54b6..f54c59e2a 100644 --- a/operationv3.go +++ b/operationv3.go @@ -364,7 +364,7 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e case OBJECT: return fmt.Errorf("%s is not supported type for %s", refType, paramType) } - case "query", "formData": + case "query": switch objectType { case ARRAY: if !IsPrimitiveType(refType) && !(refType == "file" && paramType == "formData") { @@ -417,7 +417,7 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e return nil } - case "body": + case "body", "formData": if objectType == PRIMITIVE { schema := PrimitiveSchemaV3(refType) @@ -426,7 +426,7 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e return err } - o.fillRequestBody(schema, required, description, true) + o.fillRequestBody(schema, required, description, true, paramType == "formData") return nil @@ -442,7 +442,7 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e return err } - o.fillRequestBody(schema, required, description, false) + o.fillRequestBody(schema, required, description, false, paramType == "formData") return nil @@ -464,13 +464,15 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e return nil } -func (o *OperationV3) fillRequestBody(schema *spec.RefOrSpec[spec.Schema], required bool, description string, primitive bool) { +func (o *OperationV3) fillRequestBody(schema *spec.RefOrSpec[spec.Schema], required bool, description string, primitive, formData bool) { if o.RequestBody == nil { o.RequestBody = spec.NewRequestBodySpec() o.RequestBody.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) - if primitive { + if primitive && !formData { o.RequestBody.Spec.Spec.Content["text/plain"] = spec.NewMediaType() + } else if formData { + o.RequestBody.Spec.Spec.Content["application/x-www-form-urlencoded"] = spec.NewMediaType() } else { o.RequestBody.Spec.Spec.Content["application/json"] = spec.NewMediaType() } diff --git a/operationv3_test.go b/operationv3_test.go index 7e5ad86b4..6d79ca4cf 100644 --- a/operationv3_test.go +++ b/operationv3_test.go @@ -773,7 +773,7 @@ func TestOperation_ParseParamCommentV3(t *testing.T) { t.Run("integer", func(t *testing.T) { t.Parallel() - for _, paramType := range []string{"header", "path", "query", "formData"} { + for _, paramType := range []string{"header", "path", "query"} { t.Run(paramType, func(t *testing.T) { o := NewOperationV3(New()) err := o.ParseComment(`@Param some_id `+paramType+` int true "Some ID"`, nil) @@ -808,7 +808,7 @@ func TestOperation_ParseParamCommentV3(t *testing.T) { t.Run("string", func(t *testing.T) { t.Parallel() - for _, paramType := range []string{"header", "path", "query", "formData"} { + for _, paramType := range []string{"header", "path", "query"} { t.Run(paramType, func(t *testing.T) { o := NewOperationV3(New()) err := o.ParseComment(`@Param some_string `+paramType+` string true "Some String"`, nil) @@ -842,7 +842,7 @@ func TestOperation_ParseParamCommentV3(t *testing.T) { t.Run("object", func(t *testing.T) { t.Parallel() - for _, paramType := range []string{"header", "path", "query", "formData"} { + for _, paramType := range []string{"header", "path", "query"} { t.Run(paramType, func(t *testing.T) { assert.Error(t, NewOperationV3(New()). @@ -1108,18 +1108,17 @@ func TestParseParamCommentByFormDataTypeV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - assert.Len(t, operation.Parameters, 1) + assert.Len(t, operation.Parameters, 0) + assert.NotNil(t, operation.RequestBody) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.True(t, requestBody.Spec.Spec.Required) + assert.Equal(t, "this is a test file", requestBody.Spec.Spec.Description) + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "this is a test file", parameterSpec.Description) - assert.Equal(t, "file", parameterSpec.Name) - assert.True(t, parameterSpec.Required) - assert.Equal(t, "formData", parameterSpec.In) - assert.Equal(t, typeFile, parameterSpec.Schema.Spec.Type) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, typeFile, requestBodySpec.Content["application/x-www-form-urlencoded"].Spec.Schema.Spec.Type) } func TestParseParamCommentByFormDataTypeUint64V3(t *testing.T) { @@ -1131,18 +1130,15 @@ func TestParseParamCommentByFormDataTypeUint64V3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - assert.Len(t, operation.Parameters, 1) + assert.Len(t, operation.Parameters, 0) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) + assert.Equal(t, "this is a test file", requestBody.Spec.Spec.Description) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "this is a test file", parameterSpec.Description) - assert.Equal(t, "file", parameterSpec.Name) - assert.True(t, parameterSpec.Required) - assert.Equal(t, "formData", parameterSpec.In) - assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Type) + requestBodySpec := requestBody.Spec.Spec.Content["application/x-www-form-urlencoded"].Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, typeInteger, requestBodySpec.Schema.Spec.Type) } func TestParseParamCommentByNotSupportedTypeV3(t *testing.T) { diff --git a/parser_test.go b/parser_test.go index ea29728bb..92cb0624d 100644 --- a/parser_test.go +++ b/parser_test.go @@ -2142,9 +2142,9 @@ func TestParseTypeOverrides(t *testing.T) { searchDir := "testdata/global_override" p := New(SetOverrides(map[string]string{ - "github.com/swaggo/swag/testdata/global_override/types.Application": "string", - "github.com/swaggo/swag/testdata/global_override/types.Application2": "github.com/swaggo/swag/testdata/global_override/othertypes.Application", - "github.com/swaggo/swag/testdata/global_override/types.ShouldSkip": "", + "github.com/swaggo/swag/v2/testdata/global_override/types.Application": "string", + "github.com/swaggo/swag/v2/testdata/global_override/types.Application2": "github.com/swaggo/swag/v2/testdata/global_override/othertypes.Application", + "github.com/swaggo/swag/v2/testdata/global_override/types.ShouldSkip": "", })) err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) diff --git a/parserv3_test.go b/parserv3_test.go index 70680722f..b889188cf 100644 --- a/parserv3_test.go +++ b/parserv3_test.go @@ -355,7 +355,7 @@ func TestParseSimpleApiV3(t *testing.T) { assert.NoError(t, err) paths := p.openAPI.Paths.Spec.Paths - assert.Equal(t, 14, len(paths)) + assert.Equal(t, 15, len(paths)) path := paths["/testapi/get-string-by-int/{some_id}"].Spec.Spec.Get.Spec assert.Equal(t, "get string by ID", path.Description) @@ -365,5 +365,8 @@ func TestParseSimpleApiV3(t *testing.T) { response := path.Responses.Spec.Response["200"] assert.Equal(t, "ok", response.Spec.Spec.Description) + path = paths["/FormData"].Spec.Spec.Post.Spec + assert.NotNil(t, path) + assert.NotNil(t, path.RequestBody) //TODO add asserts } diff --git a/testdata/conflict_name/expected.json b/testdata/conflict_name/expected.json index 0b3576dbc..68aa1440d 100644 --- a/testdata/conflict_name/expected.json +++ b/testdata/conflict_name/expected.json @@ -24,7 +24,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_conflict_name_model.ErrorsResponse" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_conflict_name_model.ErrorsResponse" } } } @@ -47,7 +47,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_conflict_name_model2.ErrorsResponse" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_conflict_name_model2.ErrorsResponse" } } } @@ -55,7 +55,7 @@ } }, "definitions": { - "github_com_swaggo_swag_testdata_conflict_name_model.ErrorsResponse": { + "github_com_swaggo_swag_v2_testdata_conflict_name_model.ErrorsResponse": { "type": "object", "properties": { "newTime": { @@ -63,7 +63,7 @@ } } }, - "github_com_swaggo_swag_testdata_conflict_name_model.MyStruct": { + "github_com_swaggo_swag_v2_testdata_conflict_name_model.MyStruct": { "type": "object", "properties": { "name": { @@ -71,7 +71,7 @@ } } }, - "github_com_swaggo_swag_testdata_conflict_name_model2.ErrorsResponse": { + "github_com_swaggo_swag_v2_testdata_conflict_name_model2.ErrorsResponse": { "type": "object", "properties": { "newTime": { @@ -79,7 +79,7 @@ } } }, - "github_com_swaggo_swag_testdata_conflict_name_model2.MyStruct": { + "github_com_swaggo_swag_v2_testdata_conflict_name_model2.MyStruct": { "type": "object", "properties": { "name": { @@ -91,7 +91,7 @@ "type": "object", "properties": { "my": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_conflict_name_model.MyStruct" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_conflict_name_model.MyStruct" }, "name": { "type": "string" @@ -102,7 +102,7 @@ "type": "object", "properties": { "my": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_conflict_name_model2.MyStruct" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_conflict_name_model2.MyStruct" }, "name": { "type": "string" diff --git a/testdata/generics_package_alias/internal/expected.json b/testdata/generics_package_alias/internal/expected.json index 2f6c85f52..c796453ce 100644 --- a/testdata/generics_package_alias/internal/expected.json +++ b/testdata/generics_package_alias/internal/expected.json @@ -18,7 +18,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1_ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1_ProductDto" } } } @@ -58,7 +58,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1_ProductDto" } } } @@ -98,7 +98,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1_ProductDto" } } } @@ -138,7 +138,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1_ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1_ProductDto" } } } @@ -158,7 +158,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1_ProductDto" } } } @@ -178,7 +178,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1_ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1_ProductDto" } } } @@ -198,7 +198,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1_ProductDto" } } } @@ -218,7 +218,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external1_Customer" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-external1_Customer" } } } @@ -238,7 +238,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external2_Customer" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-external2_Customer" } } } @@ -258,7 +258,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external3_Customer" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-external3_Customer" } } } @@ -278,7 +278,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external4_Customer" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-external4_Customer" } } } @@ -399,7 +399,7 @@ } } }, - "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external1_Customer": { + "github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-external1_Customer": { "type": "object", "properties": { "items1": { @@ -410,7 +410,7 @@ } } }, - "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external2_Customer": { + "github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-external2_Customer": { "type": "object", "properties": { "items1": { @@ -421,7 +421,7 @@ } } }, - "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external3_Customer": { + "github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-external3_Customer": { "type": "object", "properties": { "items1": { @@ -432,7 +432,7 @@ } } }, - "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external4_Customer": { + "github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-external4_Customer": { "type": "object", "properties": { "items1": { @@ -443,29 +443,29 @@ } } }, - "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1_ProductDto": { + "github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1_ProductDto": { "type": "object", "properties": { "items1": { "type": "array", "items": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ProductDto" } } } }, - "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto": { + "github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1_ProductDto": { "type": "object", "properties": { "items1": { "type": "array", "items": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1.ProductDto" } } } }, - "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ProductDto": { + "github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ProductDto": { "type": "object", "properties": { "name1": { @@ -473,29 +473,29 @@ } } }, - "github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1_ProductDto": { + "github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1_ProductDto": { "type": "object", "properties": { "items2": { "type": "array", "items": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path1_v1.ProductDto" } } } }, - "github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto": { + "github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1_ProductDto": { "type": "object", "properties": { "items2": { "type": "array", "items": { - "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ProductDto" + "$ref": "#/definitions/github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1.ProductDto" } } } }, - "github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ProductDto": { + "github_com_swaggo_swag_v2_testdata_generics_package_alias_internal_path2_v1.ProductDto": { "type": "object", "properties": { "name2": { diff --git a/testdata/v3/simple/api/api.go b/testdata/v3/simple/api/api.go index 116d5e86a..91433b466 100644 --- a/testdata/v3/simple/api/api.go +++ b/testdata/v3/simple/api/api.go @@ -138,3 +138,9 @@ func GetPet6FunctionScopedResponse() { Name string } } + +// @param Token formData web.Request true "Params" comment +// @Router /FormData [post] +func FormData() { + +} diff --git a/testdata/v3/simple/main.go b/testdata/v3/simple/main.go index 0b960233b..a4721c6c6 100644 --- a/testdata/v3/simple/main.go +++ b/testdata/v3/simple/main.go @@ -24,5 +24,6 @@ func main() { http.HandleFunc("/testapi/get-string-by-int/", api.GetStringByInt) http.HandleFunc("/testapi/get-struct-array-by-string/", api.GetStructArrayByString) http.HandleFunc("/testapi/upload", api.Upload) + http.ListenAndServe(":8080", nil) } diff --git a/testdata/v3/simple/web/handler.go b/testdata/v3/simple/web/handler.go index e67a67f40..3afcdd0d1 100644 --- a/testdata/v3/simple/web/handler.go +++ b/testdata/v3/simple/web/handler.go @@ -99,3 +99,16 @@ type Pet5c struct { *Pet5b Odd bool `json:"odd" binding:"required"` } + +type Request struct { + GrantType string `json:"grant_type" validate:"required"` + ClientID string `json:"client_id" validate:"required"` + ClientSecret string `json:"client_secret" validate:"required"` + RedirectURI string `json:"redirect_uri"` + Code string `json:"code"` + CodeVerifier string `json:"code_verifier"` + Scope string `json:"scope"` + Username string `json:"username"` + Password string `json:"password"` + RefreshToken string `json:"refresh_token"` +} From 45df9a3137e08bd78c04d78097fe46751860f1aa Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Tue, 18 Apr 2023 00:28:24 +0300 Subject: [PATCH 23/34] chore: fix unit tests after merging #1499 (#1558) --- gen/gen.go | 8 ++++---- gen/gen_test.go | 2 +- gen/src/oas2.tmpl | 4 +++- gen/src/oas3.tmpl | 6 ++++-- go.mod | 2 +- testdata/delims/main.go | 6 +++--- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/gen/gen.go b/gen/gen.go index 82e2e0d9c..60b3c7979 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -510,7 +510,7 @@ func (g *Gen) writeGoDocV3(packageName string, output io.Writer, openAPI *v3.Ope generator, err := template.New("oas3.tmpl").Funcs(template.FuncMap{ "printDoc": func(v string) string { // Add schemes - v = "{\n \"schemes\": {{ marshal .Schemes }}," + v[1:] + v = "{\n \"schemes\": " + config.LeftTemplateDelim + " marshal .Schemes " + config.RightTemplateDelim + "," + v[1:] // Sanitize backticks return strings.Replace(v, "`", "`+\"`\"+`", -1) }, @@ -524,9 +524,9 @@ func (g *Gen) writeGoDocV3(packageName string, output io.Writer, openAPI *v3.Ope OpenAPI: openAPI.OpenAPI, Info: &v3.Extendable[v3.Info]{ Spec: &v3.Info{ - Description: "{{escape .Description}}", - Title: "{{.Title}}", - Version: "{{.Version}}", + Description: config.LeftTemplateDelim + "escape .Description" + config.RightTemplateDelim, + Title: config.LeftTemplateDelim + ".Title" + config.RightTemplateDelim, + Version: config.LeftTemplateDelim + ".Version" + config.RightTemplateDelim, TermsOfService: openAPI.Info.Spec.TermsOfService, Contact: openAPI.Info.Spec.Contact, License: openAPI.Info.Spec.License, diff --git a/gen/gen_test.go b/gen/gen_test.go index aade104f8..55a40b4a8 100644 --- a/gen/gen_test.go +++ b/gen/gen_test.go @@ -284,7 +284,7 @@ func TestGen_BuildDocCustomDelims(t *testing.T) { } } - cmd := exec.Command("go", "build", "-buildmode=plugin", "github.com/swaggo/swag/testdata/delims") + cmd := exec.Command("go", "build", "-buildmode=plugin", "github.com/swaggo/swag/v2/testdata/delims") cmd.Dir = config.SearchDir diff --git a/gen/src/oas2.tmpl b/gen/src/oas2.tmpl index 96caa3fba..dbea4efa6 100644 --- a/gen/src/oas2.tmpl +++ b/gen/src/oas2.tmpl @@ -1,6 +1,6 @@ // Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT. -package docs +package {{.PackageName}} import "github.com/swaggo/swag/v2" @@ -16,6 +16,8 @@ var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} Description: {{ printf "%q" .Description}}, InfoInstanceName: {{ printf "%q" .InstanceName }}, SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, + LeftDelim: {{ printf "%q" .LeftTemplateDelim}}, + RightDelim: {{ printf "%q" .RightTemplateDelim}}, } func init() { diff --git a/gen/src/oas3.tmpl b/gen/src/oas3.tmpl index bc46ce7de..4f3a1a805 100644 --- a/gen/src/oas3.tmpl +++ b/gen/src/oas3.tmpl @@ -1,6 +1,6 @@ -// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT +// Code generated by swaggo/swag{{ if .GeneratedTime }} at {{ .Timestamp }}{{ end }}. DO NOT EDIT. -package docs +package {{.PackageName}} import "github.com/swaggo/swag/v2" @@ -13,6 +13,8 @@ var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} Description: {{ printf "%q" .Description}}, InfoInstanceName: {{ printf "%q" .InstanceName }}, SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, + LeftDelim: {{ printf "%q" .LeftTemplateDelim}}, + RightDelim: {{ printf "%q" .RightTemplateDelim}}, } func init() { diff --git a/go.mod b/go.mod index 5a6271167..32f79d171 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/swaggo/swag +module github.com/swaggo/swag/v2 go 1.18 diff --git a/testdata/delims/main.go b/testdata/delims/main.go index cf5268cad..b880e95a0 100644 --- a/testdata/delims/main.go +++ b/testdata/delims/main.go @@ -1,9 +1,9 @@ package main import ( - "github.com/swaggo/swag" - "github.com/swaggo/swag/testdata/delims/api" - _ "github.com/swaggo/swag/testdata/delims/docs" + "github.com/swaggo/swag/v2" + "github.com/swaggo/swag/v2/testdata/delims/api" + _ "github.com/swaggo/swag/v2/testdata/delims/docs" ) func ReadDoc() string { From ff9485a3dee94df3884693991e71ce899f58476e Mon Sep 17 00:00:00 2001 From: Nerzal Date: Wed, 19 Apr 2023 18:11:28 +0200 Subject: [PATCH 24/34] V2: Update Makefile (#1560) * Update README_zh-CN.md (#1545) remove repeat net/http * Add option to set template delimiters (#1499) * Add template action delimiter cli flag * Add delims to generator config and template Also adds tests using the "quote" test as a base. This has to have a custom Instance name or it will clash with the "quotes" one and panic since it will have registered two "swagger" instances in the package test. * Add testdata for custom delim flags Based on the "quote" testdata. * Add delims to the spec, with tests. Make sure we don't add delims if they are empty. This shouldn't be possible, but might as well be safe. * Go mod tidy and sum update * Make the CLI experience a bit cleaner * Revert go.mod and sum * Update readme * fix bug: enums of explicit type conversion (#1556) Signed-off-by: sdghchj * integrate new delimiter feature from master branch into v2 * cleanup * update makefile to use v2 path --------- Signed-off-by: sdghchj Co-authored-by: tzxdtc10 Co-authored-by: Leo Palmer Sunmo Co-authored-by: sdghchj Co-authored-by: Tobias Theel --- Makefile | 2 +- gen/src/oas2.tmpl | 2 +- gen/src/oas3.tmpl | 2 +- go.mod | 2 +- go.sum | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 04e6514e9..089c65be6 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ GOPATH:=$(shell $(GOCMD) env GOPATH) u := $(if $(update),-u) BINARY_NAME:=swag -PACKAGES:=$(shell $(GOLIST) github.com/swaggo/swag github.com/swaggo/swag/cmd/swag github.com/swaggo/swag/gen github.com/swaggo/swag/format) +PACKAGES:=$(shell $(GOLIST) github.com/swaggo/swag/v2 github.com/swaggo/swag/v2/cmd/swag github.com/swaggo/swag/v2/gen github.com/swaggo/swag/v2/format) GOFILES:=$(shell find . -name "*.go" -type f) export GO111MODULE := on diff --git a/gen/src/oas2.tmpl b/gen/src/oas2.tmpl index dbea4efa6..0446b9db4 100644 --- a/gen/src/oas2.tmpl +++ b/gen/src/oas2.tmpl @@ -17,7 +17,7 @@ var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} InfoInstanceName: {{ printf "%q" .InstanceName }}, SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, LeftDelim: {{ printf "%q" .LeftTemplateDelim}}, - RightDelim: {{ printf "%q" .RightTemplateDelim}}, + RightDelim: {{ printf "%q" .RightTemplateDelim}}, } func init() { diff --git a/gen/src/oas3.tmpl b/gen/src/oas3.tmpl index 4f3a1a805..dde742641 100644 --- a/gen/src/oas3.tmpl +++ b/gen/src/oas3.tmpl @@ -14,7 +14,7 @@ var SwaggerInfo{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }} InfoInstanceName: {{ printf "%q" .InstanceName }}, SwaggerTemplate: docTemplate{{ if ne .InstanceName "swagger" }}{{ .InstanceName }} {{- end }}, LeftDelim: {{ printf "%q" .LeftTemplateDelim}}, - RightDelim: {{ printf "%q" .RightTemplateDelim}}, + RightDelim: {{ printf "%q" .RightTemplateDelim}}, } func init() { diff --git a/go.mod b/go.mod index 32f79d171..f88abfe00 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.2 github.com/sv-tools/openapi v0.2.1 - golang.org/x/tools v0.7.0 + golang.org/x/tools v0.8.0 sigs.k8s.io/yaml v1.3.0 ) diff --git a/go.sum b/go.sum index 3c097fdc8..2565e61c0 100644 --- a/go.sum +++ b/go.sum @@ -64,11 +64,11 @@ github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 9a872fbef7d6fb907a82178958e1c7d58514d7a3 Mon Sep 17 00:00:00 2001 From: Nerzal Date: Thu, 4 May 2023 11:53:06 +0200 Subject: [PATCH 25/34] V2: Fix template delims (#1572) * Fix template delims * go mod tidy --------- Co-authored-by: Tobias Theel --- gen/gen.go | 38 +++++++++++++++++++++----------------- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/gen/gen.go b/gen/gen.go index 60b3c7979..58fcfb1a1 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -285,7 +285,7 @@ func (g *Gen) writeDoc(config *Config, doc interface{}) error { case *v3.OpenAPI: err = g.writeGoDocV3(packageName, docs, spec, config) if err != nil { - return nil + return err } } g.debug.Printf("create docs.go at %+v", docFileName) @@ -552,23 +552,27 @@ func (g *Gen) writeGoDocV3(packageName string, output io.Writer, openAPI *v3.Ope buffer := &bytes.Buffer{} err = generator.Execute(buffer, struct { - Timestamp time.Time - Doc string - PackageName string - Title string - Description string - Version string - InstanceName string - GeneratedTime bool + Timestamp time.Time + Doc string + PackageName string + Title string + Description string + Version string + InstanceName string + GeneratedTime bool + LeftTemplateDelim string + RightTemplateDelim string }{ - Timestamp: time.Now(), - GeneratedTime: config.GeneratedTime, - Doc: string(buf), - PackageName: packageName, - Title: openAPI.Info.Spec.Title, - Description: openAPI.Info.Spec.Description, - Version: openAPI.Info.Spec.Version, - InstanceName: config.InstanceName, + Timestamp: time.Now(), + GeneratedTime: config.GeneratedTime, + Doc: string(buf), + PackageName: packageName, + Title: openAPI.Info.Spec.Title, + Description: openAPI.Info.Spec.Description, + Version: openAPI.Info.Spec.Version, + InstanceName: config.InstanceName, + LeftTemplateDelim: config.LeftTemplateDelim, + RightTemplateDelim: config.RightTemplateDelim, }) if err != nil { return err diff --git a/go.mod b/go.mod index f88abfe00..8f6be0f9f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/KyleBanks/depth v1.2.1 - github.com/go-openapi/spec v0.20.8 + github.com/go-openapi/spec v0.20.9 github.com/json-iterator/go v1.1.12 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.2 diff --git a/go.sum b/go.sum index 2565e61c0..16966338a 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaL github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU= -github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= From c7b796dcb851ffca7e16b18bc91aa3696f6dadf5 Mon Sep 17 00:00:00 2001 From: Nerzal Date: Thu, 4 May 2023 13:12:22 +0200 Subject: [PATCH 26/34] V2: Add Servers param (#1573) * Fix template delims * go mod tidy * Add basic implementation of the servers object --------- Co-authored-by: Tobias Theel --- README.md | 8 ++++++++ parserv3.go | 16 ++++++++++++++++ parserv3_test.go | 23 +++++++++++++++++++++++ testdata/v3/servers/main.go | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 testdata/v3/servers/main.go diff --git a/README.md b/README.md index c9ef0effd..69e963187 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,14 @@ When a short string in your documentation is insufficient, or you need images, c | tag.name | Name of a tag.| // @tag.name This is the name of the tag | | tag.description.markdown | Description of the tag this is an alternative to tag.description. The description will be read from a file named like tagname.md | // @tag.description.markdown | +## Open API V3.1.0+ + +The following annotations are only available if you set the -v3.1 flag in the CLI. + +| annotation | description | example | +|-------------|--------------------------------------------|---------------------------------| +| servers.url | The URL of a server| // @servers.url https://petstore.example.com/api/v1 | +| servers.description | The description of a server| // @servers.description Production API | ## API Operation diff --git a/parserv3.go b/parserv3.go index d71e69050..fa494c8c7 100644 --- a/parserv3.go +++ b/parserv3.go @@ -164,6 +164,22 @@ func (p *Parser) parseGeneralAPIInfoV3(comments []string) error { } p.openAPI.Info.Extensions[originalAttribute[1:]] = valueJSON + case "@servers.url": + server := spec.NewServer() + server.Spec.URL = value + + p.openAPI.Servers = append(p.openAPI.Servers, server) + case "@servers.description": + server := p.openAPI.Servers[len(p.openAPI.Servers)-1] + server.Spec.Description = value + case "@servers.variables.enum": + p.debug.Printf("not yet implemented: @servers.variables.enum") + case "@servers.variables.default": + p.debug.Printf("not yet implemented: @servers.variables.default") + case "@servers.variables.description": + p.debug.Printf("not yet implemented: @servers.variables.description") + case "@servers.variables.description.markdown": + p.debug.Printf("not yet implemented: @servers.variables.description.markdown") default: if strings.HasPrefix(attribute, "@x-") { err := p.parseExtensionsV3(value, attribute) diff --git a/parserv3_test.go b/parserv3_test.go index b889188cf..2511ca6ce 100644 --- a/parserv3_test.go +++ b/parserv3_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestOverridesGetTypeSchemaV3(t *testing.T) { @@ -370,3 +371,25 @@ func TestParseSimpleApiV3(t *testing.T) { assert.NotNil(t, path.RequestBody) //TODO add asserts } + +func TestParserParseServers(t *testing.T) { + t.Parallel() + + searchDir := "testdata/v3/servers" + p := New(GenerateOpenAPI3Doc(true)) + p.PropNamingStrategy = PascalCase + + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + + servers := p.openAPI.Servers + require.NotNil(t, servers) + + assert.Equal(t, 2, len(servers)) + assert.Equal(t, "https://test.petstore.com/v3", servers[0].Spec.URL) + assert.Equal(t, "Test Petstore server.", servers[0].Spec.Description) + + assert.Equal(t, "https://petstore.com/v3", servers[1].Spec.URL) + assert.Equal(t, "Production Petstore server.", servers[1].Spec.Description) + +} diff --git a/testdata/v3/servers/main.go b/testdata/v3/servers/main.go new file mode 100644 index 000000000..7820c34e8 --- /dev/null +++ b/testdata/v3/servers/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/v2/testdata/v3/simple/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @servers.url https://test.petstore.com/v3 +// @servers.description Test Petstore server. + +// @servers.url https://petstore.com/v3 +// @servers.description Production Petstore server. +func main() { + http.HandleFunc("/testapi/get-string-by-int/", api.GetStringByInt) + http.HandleFunc("/testapi/get-struct-array-by-string/", api.GetStructArrayByString) + http.HandleFunc("/testapi/upload", api.Upload) + + http.ListenAndServe(":8080", nil) +} From 60ef6b74473bd88dd6f98d63bcd7b76dbf7af6c5 Mon Sep 17 00:00:00 2001 From: Nerzal Date: Wed, 31 May 2023 03:08:38 +0200 Subject: [PATCH 27/34] fix bug: enums of underscored number (#1581) (#1592) Signed-off-by: sdghchj Co-authored-by: sdghchj --- package.go | 5 +++++ testdata/enums/consts/const.go | 1 + 2 files changed, 6 insertions(+) diff --git a/package.go b/package.go index bc25786ce..6b4e3bd0d 100644 --- a/package.go +++ b/package.go @@ -5,6 +5,7 @@ import ( "go/token" "reflect" "strconv" + "strings" ) // PackageDefinitions files and definition in a package. @@ -94,6 +95,10 @@ func (pkg *PackageDefinitions) evaluateConstValue(file *ast.File, iota int, expr case *ast.BasicLit: switch valueExpr.Kind { case token.INT: + // handle underscored number, such as 1_000_000 + if strings.ContainsRune(valueExpr.Value, '_') { + valueExpr.Value = strings.Replace(valueExpr.Value, "_", "", -1) + } // hexadecimal if len(valueExpr.Value) > 2 && valueExpr.Value[0] == '0' && valueExpr.Value[1] == 'x' { if x, err := strconv.ParseInt(valueExpr.Value[2:], 16, 64); err == nil { diff --git a/testdata/enums/consts/const.go b/testdata/enums/consts/const.go index 83dc97fa7..27bfb28f5 100644 --- a/testdata/enums/consts/const.go +++ b/testdata/enums/consts/const.go @@ -10,3 +10,4 @@ const octnum = 017 const nonescapestr = `aa\nbb\u8888cc` const escapestr = "aa\nbb\u8888cc" const escapechar = '\u8888' +const underscored = 1_000_000 From 0709499c72ecdb488c136cfb8e0c0ed3b7168571 Mon Sep 17 00:00:00 2001 From: nonchan <50233291+nonchan7720@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:59:27 +0900 Subject: [PATCH 28/34] Feat servers variables of current spec (#1612) * Add Servers variables feature * Test modifications * fix lint --- parserv3.go | 59 ++++++++++++++++++++++++++++++++++--- parserv3_test.go | 7 ++++- testdata/v3/servers/main.go | 7 ++++- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/parserv3.go b/parserv3.go index fa494c8c7..225cfcee3 100644 --- a/parserv3.go +++ b/parserv3.go @@ -7,6 +7,7 @@ import ( "go/token" "net/http" "reflect" + "regexp" "sort" "strings" @@ -32,6 +33,11 @@ func (p *Parser) GetOpenAPI() *spec.OpenAPI { return p.openAPI } +var ( + serversURLPattern = regexp.MustCompile(`\{([^}]+)\}`) + serversVariablesPattern = regexp.MustCompile(`^(\w+)\s+(.+)$`) +) + func (p *Parser) parseGeneralAPIInfoV3(comments []string) error { previousAttribute := "" @@ -167,19 +173,64 @@ func (p *Parser) parseGeneralAPIInfoV3(comments []string) error { case "@servers.url": server := spec.NewServer() server.Spec.URL = value + matches := serversURLPattern.FindAllStringSubmatch(value, -1) + server.Spec.Variables = make(map[string]*spec.Extendable[spec.ServerVariable]) + for _, match := range matches { + server.Spec.Variables[match[1]] = spec.NewServerVariable() + } p.openAPI.Servers = append(p.openAPI.Servers, server) case "@servers.description": server := p.openAPI.Servers[len(p.openAPI.Servers)-1] server.Spec.Description = value case "@servers.variables.enum": - p.debug.Printf("not yet implemented: @servers.variables.enum") + server := p.openAPI.Servers[len(p.openAPI.Servers)-1] + matches := serversVariablesPattern.FindStringSubmatch(value) + if len(matches) > 0 { + variable, ok := server.Spec.Variables[matches[1]] + if !ok { + p.debug.Printf("Variables are not detected.") + continue + } + variable.Spec.Enum = append(variable.Spec.Enum, matches[2]) + } case "@servers.variables.default": - p.debug.Printf("not yet implemented: @servers.variables.default") + server := p.openAPI.Servers[len(p.openAPI.Servers)-1] + matches := serversVariablesPattern.FindStringSubmatch(value) + if len(matches) > 0 { + variable, ok := server.Spec.Variables[matches[1]] + if !ok { + p.debug.Printf("Variables are not detected.") + continue + } + variable.Spec.Default = matches[2] + } case "@servers.variables.description": - p.debug.Printf("not yet implemented: @servers.variables.description") + server := p.openAPI.Servers[len(p.openAPI.Servers)-1] + matches := serversVariablesPattern.FindStringSubmatch(value) + if len(matches) > 0 { + variable, ok := server.Spec.Variables[matches[1]] + if !ok { + p.debug.Printf("Variables are not detected.") + continue + } + variable.Spec.Default = matches[2] + } case "@servers.variables.description.markdown": - p.debug.Printf("not yet implemented: @servers.variables.description.markdown") + server := p.openAPI.Servers[len(p.openAPI.Servers)-1] + matches := serversVariablesPattern.FindStringSubmatch(value) + if len(matches) > 0 { + variable, ok := server.Spec.Variables[matches[1]] + if !ok { + p.debug.Printf("Variables are not detected.") + continue + } + commentInfo, err := getMarkdownForTag(matches[1], p.markdownFileDir) + if err != nil { + return err + } + variable.Spec.Description = string(commentInfo) + } default: if strings.HasPrefix(attribute, "@x-") { err := p.parseExtensionsV3(value, attribute) diff --git a/parserv3_test.go b/parserv3_test.go index 2511ca6ce..38bf4453b 100644 --- a/parserv3_test.go +++ b/parserv3_test.go @@ -386,9 +386,14 @@ func TestParserParseServers(t *testing.T) { require.NotNil(t, servers) assert.Equal(t, 2, len(servers)) - assert.Equal(t, "https://test.petstore.com/v3", servers[0].Spec.URL) + assert.Equal(t, "{scheme}://{host}:{port}", servers[0].Spec.URL) assert.Equal(t, "Test Petstore server.", servers[0].Spec.Description) + assert.Equal(t, "https", servers[0].Spec.Variables["scheme"].Spec.Default) + assert.Equal(t, []string{"http", "https"}, servers[0].Spec.Variables["scheme"].Spec.Enum) + assert.Equal(t, "test.petstore.com", servers[0].Spec.Variables["host"].Spec.Default) + assert.Equal(t, "443", servers[0].Spec.Variables["port"].Spec.Default) + assert.Equal(t, "https://petstore.com/v3", servers[1].Spec.URL) assert.Equal(t, "Production Petstore server.", servers[1].Spec.Description) diff --git a/testdata/v3/servers/main.go b/testdata/v3/servers/main.go index 7820c34e8..f08733123 100644 --- a/testdata/v3/servers/main.go +++ b/testdata/v3/servers/main.go @@ -18,8 +18,13 @@ import ( // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @servers.url https://test.petstore.com/v3 +// @servers.url {scheme}://{host}:{port} // @servers.description Test Petstore server. +// @servers.variables.enum scheme http +// @servers.variables.enum scheme https +// @servers.variables.default scheme https +// @servers.variables.default host test.petstore.com +// @servers.variables.default port 443 // @servers.url https://petstore.com/v3 // @servers.description Production Petstore server. From 86df652f4767478c14c94f6c126a7e2fc60f5072 Mon Sep 17 00:00:00 2001 From: Seehait Chockthanyawat Date: Tue, 7 Nov 2023 16:32:26 +0900 Subject: [PATCH 29/34] fix: avoid shadowed variable on operationv3 (#1680) --- operationv3.go | 12 +++--- operationv3_test.go | 90 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/operationv3.go b/operationv3.go index f54c59e2a..4737694e1 100644 --- a/operationv3.go +++ b/operationv3.go @@ -388,27 +388,29 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e continue } + itemParam := param // Avoid shadowed variable which could cause side effects to o.Operation.Parameters + switch { case prop.Type[0] == ARRAY && prop.Items.Schema != nil && len(prop.Items.Schema.Spec.Type) > 0 && IsSimplePrimitiveType(prop.Items.Schema.Spec.Type[0]): - param = createParameterV3(paramType, prop.Description, name, prop.Type[0], prop.Items.Schema.Spec.Type[0], findInSlice(schema.Spec.Required, name), enums, o.parser.collectionFormatInQuery) + itemParam = createParameterV3(paramType, prop.Description, name, prop.Type[0], prop.Items.Schema.Spec.Type[0], findInSlice(schema.Spec.Required, name), enums, o.parser.collectionFormatInQuery) case IsSimplePrimitiveType(prop.Type[0]): - param = createParameterV3(paramType, prop.Description, name, PRIMITIVE, prop.Type[0], findInSlice(schema.Spec.Required, name), enums, o.parser.collectionFormatInQuery) + itemParam = createParameterV3(paramType, prop.Description, name, PRIMITIVE, prop.Type[0], findInSlice(schema.Spec.Required, name), enums, o.parser.collectionFormatInQuery) default: o.parser.debug.Printf("skip field [%s] in %s is not supported type for %s", name, refType, paramType) continue } - param.Schema.Spec = prop + itemParam.Schema.Spec = prop listItem := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ Spec: &spec.Extendable[spec.Parameter]{ - Spec: ¶m, + Spec: &itemParam, }, } @@ -710,7 +712,7 @@ func (o *OperationV3) parseAPIObjectSchema(commentLine, schemaType, refType stri result := spec.NewSchemaSpec() result.Spec.Type = spec.NewSingleOrArray("array") - result.Spec.Items = spec.NewBoolOrSchema(false, schema) //TODO: allowed? + result.Spec.Items = spec.NewBoolOrSchema(false, schema) // TODO: allowed? return result, nil default: diff --git a/operationv3_test.go b/operationv3_test.go index 6d79ca4cf..8408aa34f 100644 --- a/operationv3_test.go +++ b/operationv3_test.go @@ -1,6 +1,7 @@ package swag import ( + "go/ast" goparser "go/parser" "go/token" "testing" @@ -852,6 +853,95 @@ func TestOperation_ParseParamCommentV3(t *testing.T) { } }) + t.Run("struct queries", func(t *testing.T) { + t.Parallel() + parser := New() + parser.packages.uniqueDefinitions["main.Object"] = &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{ + List: []*ast.Field{ + { + Names: []*ast.Ident{ + {Name: "T"}, + }, + Type: ast.NewIdent("string"), + }, + { + Names: []*ast.Ident{ + {Name: "T2"}, + }, + Type: ast.NewIdent("string"), + }, + }, + }, + }, + }, + } + o := NewOperationV3(parser) + err := o.ParseComment(`@Param some_object query main.Object true "Some Object"`, + nil) + + assert.NoError(t, err) + + expectedT := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ + Spec: &spec.Extendable[spec.Parameter]{ + Spec: &spec.Parameter{ + Name: "t", + In: "query", + Schema: &spec.RefOrSpec[spec.Schema]{ + Spec: &spec.Schema{ + JsonSchema: spec.JsonSchema{ + JsonSchemaCore: spec.JsonSchemaCore{ + Type: typeString, + }, + }, + }, + }, + }, + }, + } + expectedT2 := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ + Spec: &spec.Extendable[spec.Parameter]{ + Spec: &spec.Parameter{ + Name: "t2", + In: "query", + Schema: &spec.RefOrSpec[spec.Schema]{ + Spec: &spec.Schema{ + JsonSchema: spec.JsonSchema{ + JsonSchemaCore: spec.JsonSchemaCore{ + Type: typeString, + }, + }, + }, + }, + }, + }, + } + + assert.Len(t, o.Parameters, 2) + tFound := false + t2Found := false + for _, param := range o.Parameters { + switch param.Spec.Spec.Name { + case "t": + assert.EqualValues(t, expectedT, param) + tFound = true + case "t2": + assert.EqualValues(t, expectedT2, param) + t2Found = true + default: + assert.Fail(t, "unexpected result") + } + } + + assert.True(t, tFound, "results should contain t") + assert.True(t, t2Found, "results should contain t2") + }) } // Test ParseParamComment Query Params From 37dac6759c21e9da93aac44c16baf4b547a59ab8 Mon Sep 17 00:00:00 2001 From: Artem Vozhzhov Date: Tue, 7 Nov 2023 14:34:05 +0700 Subject: [PATCH 30/34] fix: fixed definition parsing for alias types (#1688) --- parserv3.go | 2 +- parserv3_test.go | 21 ++++ testdata/v3/type_alias_definition/api/api.go | 10 ++ .../v3/type_alias_definition/expected.json | 110 ++++++++++++++++++ testdata/v3/type_alias_definition/main.go | 4 + .../type_alias_definition/othertypes/types.go | 5 + .../v3/type_alias_definition/types/types.go | 26 +++++ 7 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 testdata/v3/type_alias_definition/api/api.go create mode 100644 testdata/v3/type_alias_definition/expected.json create mode 100644 testdata/v3/type_alias_definition/main.go create mode 100644 testdata/v3/type_alias_definition/othertypes/types.go create mode 100644 testdata/v3/type_alias_definition/types/types.go diff --git a/parserv3.go b/parserv3.go index 225cfcee3..d7953e555 100644 --- a/parserv3.go +++ b/parserv3.go @@ -792,7 +792,7 @@ func (p *Parser) parseTypeExprV3(file *ast.File, typeExpr ast.Expr, ref bool) (* // type Foo Baz case *ast.Ident: - result, err := p.getTypeSchemaV3(expr.Name, file, true) + result, err := p.getTypeSchemaV3(expr.Name, file, ref) if err != nil { return nil, errors.Wrap(err, errMessage) } diff --git a/parserv3_test.go b/parserv3_test.go index 38bf4453b..3123f25f3 100644 --- a/parserv3_test.go +++ b/parserv3_test.go @@ -1,8 +1,10 @@ package swag import ( + "encoding/json" "go/ast" "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -398,3 +400,22 @@ func TestParserParseServers(t *testing.T) { assert.Equal(t, "Production Petstore server.", servers[1].Spec.Description) } + +func TestParseTypeAlias(t *testing.T) { + t.Parallel() + + searchDir := "testdata/v3/type_alias_definition" + + p := New(GenerateOpenAPI3Doc(true)) + + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + require.NoError(t, err) + + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) + require.NoError(t, err) + + result, err := json.Marshal(p.openAPI) + require.NoError(t, err) + + assert.JSONEq(t, string(expected), string(result)) +} diff --git a/testdata/v3/type_alias_definition/api/api.go b/testdata/v3/type_alias_definition/api/api.go new file mode 100644 index 000000000..771d21724 --- /dev/null +++ b/testdata/v3/type_alias_definition/api/api.go @@ -0,0 +1,10 @@ +package api + +import "net/http" + +// @Router /test [GET] +// @Produce json +// @Success 200 {object} types.Response "Success" +func Handle(w http.ResponseWriter, r *http.Request) { + +} diff --git a/testdata/v3/type_alias_definition/expected.json b/testdata/v3/type_alias_definition/expected.json new file mode 100644 index 000000000..f43fb9c3d --- /dev/null +++ b/testdata/v3/type_alias_definition/expected.json @@ -0,0 +1,110 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Swagger Example API", + "version": "1.0" + }, + "externalDocs": { + "description": "", + "url": "" + }, + "components": { + "schemas": { + "types.NestedStruct": { + "type": "object", + "properties": { + "int": { + "type": "integer" + } + } + }, + "types.Struct": { + "type": "object", + "properties": { + "string": { + "type": "string" + }, + "nestedStruct": { + "$ref": "#/components/schemas/types.NestedStruct" + } + } + }, + "types.StructAlias": { + "type": "object", + "properties": { + "string": { + "type": "string" + }, + "nestedStruct": { + "$ref": "#/components/schemas/types.NestedStruct" + } + } + }, + "types.StructSubtype": { + "type": "object", + "properties": { + "string": { + "type": "string" + }, + "nestedStruct": { + "$ref": "#/components/schemas/types.NestedStruct" + } + } + }, + "types.OtherStructAlias": { + "type": "object", + "properties": { + "float": { + "type": "number" + } + } + }, + "types.OtherStructSubtype": { + "type": "object", + "properties": { + "float": { + "type": "number" + } + } + }, + "types.Response": { + "type": "object", + "properties": { + "struct": { + "$ref": "#/components/schemas/types.Struct" + }, + "structAlias": { + "$ref": "#/components/schemas/types.StructAlias" + }, + "structSubtype": { + "$ref": "#/components/schemas/types.StructSubtype" + }, + "otherStructAlias": { + "$ref": "#/components/schemas/types.OtherStructAlias" + }, + "otherStructSubtype": { + "$ref": "#/components/schemas/types.OtherStructSubtype" + } + } + } + } + }, + "paths": { + "/test": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/types.Response" + } + } + }, + "description": "Success" + } + } + } + } + } +} \ No newline at end of file diff --git a/testdata/v3/type_alias_definition/main.go b/testdata/v3/type_alias_definition/main.go new file mode 100644 index 000000000..4082f2350 --- /dev/null +++ b/testdata/v3/type_alias_definition/main.go @@ -0,0 +1,4 @@ +package main + +// @title Swagger Example API +// @version 1.0 diff --git a/testdata/v3/type_alias_definition/othertypes/types.go b/testdata/v3/type_alias_definition/othertypes/types.go new file mode 100644 index 000000000..1e28d75fb --- /dev/null +++ b/testdata/v3/type_alias_definition/othertypes/types.go @@ -0,0 +1,5 @@ +package othertypes + +type Struct struct { + Float float64 +} diff --git a/testdata/v3/type_alias_definition/types/types.go b/testdata/v3/type_alias_definition/types/types.go new file mode 100644 index 000000000..d7e2f5b0b --- /dev/null +++ b/testdata/v3/type_alias_definition/types/types.go @@ -0,0 +1,26 @@ +package types + +import "github.com/swaggo/swag/v2/testdata/v3/type_alias_definition/othertypes" + +type Struct struct { + String string `json:"string"` + NestedStruct NestedStruct `json:"nestedStruct"` +} + +type NestedStruct struct { + Int int `json:"int"` +} + +type StructAlias = Struct +type OtherStructAlias = othertypes.Struct + +type StructSubtype Struct +type OtherStructSubtype othertypes.Struct + +type Response struct { + Struct `json:"struct"` + StructAlias `json:"structAlias"` + OtherStructAlias `json:"otherStructAlias"` + StructSubtype `json:"structSubtype"` + OtherStructSubtype `json:"otherStructSubtype"` +} From 8f63cde078b3938cc007ccdb956fb6747b076c5c Mon Sep 17 00:00:00 2001 From: KKKIIO Date: Tue, 7 Nov 2023 15:40:14 +0800 Subject: [PATCH 31/34] Support openapiV3 oneOf for fields/responses (#1671) * fix: drive new schema from ref and field tags * feat: support openapi-v3 oneOf tag * feat: support openapi-v3 oneOf for response --- field_parser_v3_test.go | 67 ++++++++++++++++ field_parserv3.go | 25 +++++- go.mod | 4 +- go.sum | 10 +-- operation.go | 1 + operationv3.go | 128 ++++++++++++++++++------------ parserv3.go | 6 +- parserv3_test.go | 94 +++++++++++++++++++++- testdata/v3/simple/api/api.go | 16 ++++ testdata/v3/simple/web/handler.go | 15 ++++ 10 files changed, 302 insertions(+), 64 deletions(-) diff --git a/field_parser_v3_test.go b/field_parser_v3_test.go index da544b9b2..8b0b80cf4 100644 --- a/field_parser_v3_test.go +++ b/field_parser_v3_test.go @@ -16,6 +16,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" example:"one"`, }}, @@ -27,6 +28,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" example:""`, }}, @@ -38,6 +40,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"float"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" example:"one"`, }}, @@ -52,6 +55,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" format:"csv"`, }}, @@ -65,6 +69,7 @@ func TestDefaultFieldParserV3(t *testing.T) { got, err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" binding:"required"`, }}, @@ -74,6 +79,7 @@ func TestDefaultFieldParserV3(t *testing.T) { got, err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required"`, }}, @@ -89,6 +95,7 @@ func TestDefaultFieldParserV3(t *testing.T) { &Parser{ RequiredByDefault: true, }, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test"`, }}, @@ -104,6 +111,7 @@ func TestDefaultFieldParserV3(t *testing.T) { &Parser{ RequiredByDefault: true, }, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" binding:"optional"`, }}, @@ -115,6 +123,7 @@ func TestDefaultFieldParserV3(t *testing.T) { &Parser{ RequiredByDefault: true, }, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"optional"`, }}, @@ -131,6 +140,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Extensions = map[string]interface{}{} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" extensions:"x-nullable,x-abc=def,!x-omitempty,x-example=[0, 9],x-example2={çãíœ, (bar=(abc, def)), [0,9]}"`, }}, @@ -150,6 +160,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" enums:"a,b,c"`, }}, @@ -161,6 +172,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"float"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" enums:"a,b,c"`, }}, @@ -177,6 +189,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Enum = []interface{}{} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" enums:"0,1,2" x-enum-varnames:"Daily,Weekly,Monthly"`, }}, @@ -188,6 +201,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"int"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" enums:"0,1,2,3" x-enum-varnames:"Daily,Weekly,Monthly"`, }}, @@ -204,6 +218,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Enum = []interface{}{} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" enums:"0,1,2" x-enum-varnames:"Daily,Weekly,Monthly"`, }}, @@ -220,6 +235,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" default:"pass"`, }}, @@ -231,6 +247,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"float"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" default:"pass"`, }}, @@ -245,6 +262,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maximum:"1"`, }}, @@ -257,6 +275,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maximum:"one"`, }}, @@ -267,6 +286,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"number"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maximum:"1"`, }}, @@ -279,6 +299,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"number"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maximum:"one"`, }}, @@ -289,6 +310,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"number"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" multipleOf:"1"`, }}, @@ -301,6 +323,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"number"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" multipleOf:"one"`, }}, @@ -311,6 +334,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" minimum:"1"`, }}, @@ -323,6 +347,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" minimum:"one"`, }}, @@ -337,6 +362,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maxLength:"1"`, }}, @@ -349,6 +375,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maxLength:"one"`, }}, @@ -359,6 +386,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" minLength:"1"`, }}, @@ -371,6 +399,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" minLength:"one"`, }}, @@ -385,6 +414,7 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" readonly:"true"`, }}, @@ -392,6 +422,24 @@ func TestDefaultFieldParserV3(t *testing.T) { assert.NoError(t, err) assert.Equal(t, true, schema.Spec.ReadOnly) }) + + t.Run("OneOf tag", func(t *testing.T) { + t.Parallel() + + schema := spec.NewSchemaSpec() + schema.Spec.Type = []string{ANY} + err := newTagBaseFieldParserV3( + &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" oneOf:"string,float64"`, + }}, + ).ComplementSchema(schema) + assert.NoError(t, err) + assert.Len(t, schema.Spec.OneOf, 2) + assert.Equal(t, spec.NewSingleOrArray("string"), schema.Spec.OneOf[0].Spec.Type) + assert.Equal(t, spec.NewSingleOrArray("number"), schema.Spec.OneOf[1].Spec.Type) + }) } func TestValidTagsV3(t *testing.T) { @@ -402,6 +450,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=10,min=1"`, }}, @@ -416,6 +465,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=10,gte=1"`, }}, @@ -428,6 +478,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=10,min=1"`, }}, @@ -446,6 +497,7 @@ func TestValidTagsV3(t *testing.T) { err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=10,min=1"`, }}, @@ -457,6 +509,7 @@ func TestValidTagsV3(t *testing.T) { // wrong validate tag will be ignored. err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=ten,min=1"`, }}, @@ -473,6 +526,7 @@ func TestValidTagsV3(t *testing.T) { err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof='red book' 'green book'"`, }}, @@ -484,6 +538,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof=1 2 3"`, }}, @@ -499,6 +554,7 @@ func TestValidTagsV3(t *testing.T) { err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof=red green yellow"`, }}, @@ -510,6 +566,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof='red green' blue 'c0x2Cc' 'd0x7Cd'"`, }}, @@ -521,6 +578,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof='c0x9Ab' book"`, }}, @@ -532,6 +590,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" binding:"oneof=foo bar" validate:"required,oneof=foo bar" enums:"a,b,c"`, }}, @@ -543,6 +602,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" binding:"oneof=aa bb" validate:"required,oneof=foo bar"`, }}, @@ -560,6 +620,7 @@ func TestValidTagsV3(t *testing.T) { err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,unique"`, }}, @@ -577,6 +638,7 @@ func TestValidTagsV3(t *testing.T) { err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,unique,max=10,min=1,oneof=a0x2Cc 'c0x7Cd book',omitempty,dive,max=1"`, }}, @@ -597,6 +659,7 @@ func TestValidTagsV3(t *testing.T) { err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof=,max=10=90,min=1"`, }}, @@ -612,6 +675,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Items.Schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=10,min=one"`, }}, @@ -624,6 +688,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof=one two"`, }}, @@ -641,6 +706,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Items.Schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" pattern:"^[a-zA-Z0-9_]*$"`, }}, @@ -656,6 +722,7 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = typeString err := newTagBaseFieldParserV3( &Parser{}, + &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" pattern:"^[a-zA-Z0-9_]*$"`, }}, diff --git a/field_parserv3.go b/field_parserv3.go index 733907f4b..37a9c646d 100644 --- a/field_parserv3.go +++ b/field_parserv3.go @@ -83,13 +83,15 @@ func (sf *structFieldV3) setMax(valValue string) { type tagBaseFieldParserV3 struct { p *Parser + file *ast.File field *ast.Field tag reflect.StructTag } -func newTagBaseFieldParserV3(p *Parser, field *ast.Field) FieldParserV3 { +func newTagBaseFieldParserV3(p *Parser, file *ast.File, field *ast.Field) FieldParserV3 { fieldParser := tagBaseFieldParserV3{ p: p, + file: file, field: field, tag: "", } @@ -134,9 +136,10 @@ func (ps *tagBaseFieldParserV3) ComplementSchema(schema *spec.RefOrSpec[spec.Sch if err != nil { return err } - // if !reflect.ValueOf(newSchema).IsZero() { - // *schema = *(newSchema.WithAllOf(*schema.Spec)) - // } + if !reflect.ValueOf(newSchema).IsZero() { + newSchema.AllOf = []*spec.RefOrSpec[spec.Schema]{{Spec: schema.Spec}} + *schema = spec.RefOrSpec[spec.Schema]{Spec: &newSchema} + } return nil } @@ -339,6 +342,19 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st } } + var oneOfSchemas []*spec.RefOrSpec[spec.Schema] + oneOfTagValue := ps.tag.Get(oneOfTag) + if oneOfTagValue != "" { + oneOfTypes := strings.Split((oneOfTagValue), ",") + for _, oneOfType := range oneOfTypes { + oneOfSchema, err := ps.p.getTypeSchemaV3(oneOfType, ps.file, true) + if err != nil { + return fmt.Errorf("can't find oneOf type %q: %v", oneOfType, err) + } + oneOfSchemas = append(oneOfSchemas, oneOfSchema) + } + } + elemSchema := schema if field.schemaType == ARRAY { @@ -362,6 +378,7 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st elemSchema.MinLength = field.minLength elemSchema.Enum = field.enums elemSchema.Pattern = field.pattern + elemSchema.OneOf = oneOfSchemas return nil } diff --git a/go.mod b/go.mod index 8f6be0f9f..0ba74277f 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.2 github.com/sv-tools/openapi v0.2.1 - golang.org/x/tools v0.8.0 + golang.org/x/tools v0.13.0 sigs.k8s.io/yaml v1.3.0 ) @@ -30,7 +30,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/urfave/cli/v2 v2.25.1 - golang.org/x/sys v0.7.0 // indirect + golang.org/x/sys v0.12.0 // indirect gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 16966338a..298a46f74 100644 --- a/go.sum +++ b/go.sum @@ -64,11 +64,11 @@ github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= -golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/operation.go b/operation.go index 7569ee790..57850f27e 100644 --- a/operation.go +++ b/operation.go @@ -423,6 +423,7 @@ const ( extensionsTag = "extensions" collectionFormatTag = "collectionFormat" patternTag = "pattern" + oneOfTag = "oneOf" ) var regexAttributes = map[string]*regexp.Regexp{ diff --git a/operationv3.go b/operationv3.go index 4737694e1..503ef4653 100644 --- a/operationv3.go +++ b/operationv3.go @@ -5,6 +5,7 @@ import ( "fmt" "go/ast" "log" + "maps" "net/http" "strconv" "strings" @@ -926,22 +927,15 @@ func (o *OperationV3) ParseResponseComment(commentLine string, astFile *ast.File for _, codeStr := range strings.Split(matches[1], ",") { if strings.EqualFold(codeStr, defaultTag) { - response := o.DefaultResponse() - response.Description = description - - mimeType := "application/json" // TODO: set correct mimeType - setResponseSchema(response, mimeType, schema) - - continue - } - - code, err := strconv.Atoi(codeStr) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) - } - - if description == "" { - description = http.StatusText(code) + codeStr = "" + } else { + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + if description == "" { + description = http.StatusText(code) + } } response := spec.NewResponseSpec() @@ -979,15 +973,12 @@ func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error { for _, codeStr := range strings.Split(matches[1], ",") { if strings.EqualFold(codeStr, defaultTag) { - response := o.DefaultResponse() - response.Description = description - - continue - } - - _, err := strconv.Atoi(codeStr) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + codeStr = "" + } else { + _, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } } o.AddResponse(codeStr, newResponseWithDescription(description)) @@ -996,21 +987,10 @@ func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error { return nil } -// DefaultResponse return the default response member pointer. -func (o *OperationV3) DefaultResponse() *spec.Response { - if o.Responses.Spec.Default == nil { - o.Responses.Spec.Default = spec.NewResponseSpec() - o.Responses.Spec.Default.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) - } - - if o.Responses.Spec.Default.Spec.Spec.Content == nil { - o.Responses.Spec.Default.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) - } - - return o.Responses.Spec.Default.Spec.Spec -} - // AddResponse add a response for a code. +// If the code is already exist, it will merge with the old one: +// 1. The description will be replaced by the new one if the new one is not empty. +// 2. The content schema will be merged using `oneOf` if the new one is not empty. func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Extendable[spec.Response]]) { if response.Spec.Spec.Headers == nil { response.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) @@ -1020,24 +1000,74 @@ func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Ext o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]]) } - o.Responses.Spec.Response[code] = response + res := response + var prev *spec.RefOrSpec[spec.Extendable[spec.Response]] + if code != "" { + prev = o.Responses.Spec.Response[code] + } else { + prev = o.Responses.Spec.Default + } + if prev != nil { // merge into prev + res = prev + if response.Spec.Spec.Description != "" { + prev.Spec.Spec.Description = response.Spec.Spec.Description + } + if len(response.Spec.Spec.Content) > 0 { + // responses should only have one content type + singleKey := "" + for k := range response.Spec.Spec.Content { + singleKey = k + break + } + if prevMediaType := prev.Spec.Spec.Content[singleKey]; prevMediaType == nil { + prev.Spec.Spec.Content = response.Spec.Spec.Content + } else { + newMediaType := response.Spec.Spec.Content[singleKey] + if len(newMediaType.Extensions) > 0 { + if prevMediaType.Extensions == nil { + prevMediaType.Extensions = make(map[string]interface{}) + } + maps.Copy(prevMediaType.Extensions, newMediaType.Extensions) + } + if len(newMediaType.Spec.Examples) > 0 { + if prevMediaType.Spec.Examples == nil { + prevMediaType.Spec.Examples = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Example]]) + } + maps.Copy(prevMediaType.Spec.Examples, newMediaType.Spec.Examples) + } + if prevSchema := prevMediaType.Spec.Schema; prevSchema.Ref != nil || prevSchema.Spec.OneOf == nil { + oneOfSchema := spec.NewSchemaSpec() + oneOfSchema.Spec.OneOf = []*spec.RefOrSpec[spec.Schema]{prevSchema, newMediaType.Spec.Schema} + prevMediaType.Spec.Schema = oneOfSchema + } else { + prevSchema.Spec.OneOf = append(prevSchema.Spec.OneOf, newMediaType.Spec.Schema) + } + } + } + } + + if code != "" { + o.Responses.Spec.Response[code] = res + } else { + o.Responses.Spec.Default = res + } } // ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200. func (o *OperationV3) ParseEmptyResponseOnly(commentLine string) error { for _, codeStr := range strings.Split(commentLine, ",") { + var description string if strings.EqualFold(codeStr, defaultTag) { - _ = o.DefaultResponse() - - continue - } - - code, err := strconv.Atoi(codeStr) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + codeStr = "" + } else { + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + description = http.StatusText(code) } - o.AddResponse(codeStr, newResponseWithDescription(http.StatusText(code))) + o.AddResponse(codeStr, newResponseWithDescription(description)) } return nil diff --git a/parserv3.go b/parserv3.go index d7953e555..33aef2c31 100644 --- a/parserv3.go +++ b/parserv3.go @@ -15,8 +15,8 @@ import ( "github.com/sv-tools/openapi/spec" ) -// FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 create FieldParser. -type FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 +// FieldParserFactoryV3 create FieldParser. +type FieldParserFactoryV3 func(ps *Parser, file *ast.File, field *ast.Field) FieldParserV3 // FieldParserV3 parse struct field. type FieldParserV3 interface { @@ -903,7 +903,7 @@ func (p *Parser) parseStructFieldV3(file *ast.File, field *ast.Field) (map[strin } } - ps := p.fieldParserFactoryV3(p, field) + ps := p.fieldParserFactoryV3(p, file, field) if ps.ShouldSkip() { return nil, nil, nil diff --git a/parserv3_test.go b/parserv3_test.go index 3123f25f3..af56f04cd 100644 --- a/parserv3_test.go +++ b/parserv3_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/sv-tools/openapi/spec" ) func TestOverridesGetTypeSchemaV3(t *testing.T) { @@ -358,7 +359,6 @@ func TestParseSimpleApiV3(t *testing.T) { assert.NoError(t, err) paths := p.openAPI.Paths.Spec.Paths - assert.Equal(t, 15, len(paths)) path := paths["/testapi/get-string-by-int/{some_id}"].Spec.Spec.Get.Spec assert.Equal(t, "get string by ID", path.Description) @@ -372,6 +372,98 @@ func TestParseSimpleApiV3(t *testing.T) { assert.NotNil(t, path) assert.NotNil(t, path.RequestBody) //TODO add asserts + + t.Run("Test parse struct oneOf", func(t *testing.T) { + t.Parallel() + + assert.Contains(t, p.openAPI.Components.Spec.Schemas, "web.OneOfTest") + schema := p.openAPI.Components.Spec.Schemas["web.OneOfTest"].Spec + expected := `{ + "properties": { + "big_int": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "pet_detail": { + "oneOf": [ + { + "$ref": "#/components/schemas/web.Cat" + }, + { + "$ref": "#/components/schemas/web.Dog" + } + ] + } + }, + "type": "object" +}` + out, err := json.MarshalIndent(schema, "", " ") + assert.NoError(t, err) + assert.Equal(t, expected, string(out)) + + assert.Contains(t, p.openAPI.Components.Spec.Schemas, "web.Cat") + schema = p.openAPI.Components.Spec.Schemas["web.Cat"].Spec + expected = `{ + "properties": { + "age": { + "type": "integer" + }, + "hunts": { + "type": "boolean" + } + }, + "type": "object" +}` + out, err = json.MarshalIndent(schema, "", " ") + assert.NoError(t, err) + assert.Equal(t, expected, string(out)) + + assert.Contains(t, p.openAPI.Components.Spec.Schemas, "web.Dog") + schema = p.openAPI.Components.Spec.Schemas["web.Dog"].Spec + expected = `{ + "properties": { + "bark": { + "type": "boolean" + }, + "breed": { + "enum": [ + "Dingo", + "Husky", + "Retriever", + "Shepherd" + ], + "type": "string" + } + }, + "type": "object" +}` + out, err = json.MarshalIndent(schema, "", " ") + assert.NoError(t, err) + assert.Equal(t, expected, string(out)) + }) + + t.Run("Test parse response oneOf", func(t *testing.T) { + t.Parallel() + + assert.Contains(t, paths, "/pets/{id}") + path := paths["/pets/{id}"] + assert.Contains(t, path.Spec.Spec.Get.Spec.Responses.Spec.Response, "200") + response = path.Spec.Spec.Get.Spec.Responses.Spec.Response["200"] + assert.Equal(t, "Return Cat or Dog", response.Spec.Spec.Description) + mediaType := response.Spec.Spec.Content["application/json"] + rootSchema := mediaType.Spec.Schema.Spec + assert.Equal(t, []*spec.RefOrSpec[spec.Schema]{ + {Ref: &spec.Ref{Ref: "#/components/schemas/web.Cat"}}, + {Ref: &spec.Ref{Ref: "#/components/schemas/web.Dog"}}, + }, rootSchema.OneOf) + + }) } func TestParserParseServers(t *testing.T) { diff --git a/testdata/v3/simple/api/api.go b/testdata/v3/simple/api/api.go index 91433b466..39ca8bad6 100644 --- a/testdata/v3/simple/api/api.go +++ b/testdata/v3/simple/api/api.go @@ -144,3 +144,19 @@ func GetPet6FunctionScopedResponse() { func FormData() { } + +// @Success 200 {object} web.OneOfTest +// @Router /GetOneOfTypes [get] +func GetOneOfTypes() { + +} + +// @Summary Get pet by ID +// @Param id path string true "ID" +// @Success 200 {object} web.Cat +// @Success 200 {object} web.Dog +// @Success 200 "Return Cat or Dog" +// @Router /pets/{id} [get] +func GetPetByID() { + +} diff --git a/testdata/v3/simple/web/handler.go b/testdata/v3/simple/web/handler.go index 3afcdd0d1..a8e03ac9d 100644 --- a/testdata/v3/simple/web/handler.go +++ b/testdata/v3/simple/web/handler.go @@ -112,3 +112,18 @@ type Request struct { Password string `json:"password"` RefreshToken string `json:"refresh_token"` } + +type OneOfTest struct { + BigInt any `json:"big_int" oneOf:"string,int64"` + PetDetail any `json:"pet_detail" oneOf:"Cat,Dog"` +} + +type Dog struct { + Bark bool `json:"bark"` + Breed string `json:"breed" enums:"Dingo,Husky,Retriever,Shepherd"` +} + +type Cat struct { + Hunts bool `json:"hunts"` + Age int `json:"age"` +} From 687052e3c94c1e0866230e952071b8a84afa09f4 Mon Sep 17 00:00:00 2001 From: Bogdan U Date: Tue, 7 Nov 2023 09:46:17 +0200 Subject: [PATCH 32/34] Revert "Support openapiV3 oneOf for fields/responses (#1671)" (#1699) This reverts commit 8f63cde078b3938cc007ccdb956fb6747b076c5c. --- field_parser_v3_test.go | 67 ---------------- field_parserv3.go | 25 +----- go.mod | 4 +- go.sum | 10 +-- operation.go | 1 - operationv3.go | 128 ++++++++++++------------------ parserv3.go | 6 +- parserv3_test.go | 94 +--------------------- testdata/v3/simple/api/api.go | 16 ---- testdata/v3/simple/web/handler.go | 15 ---- 10 files changed, 64 insertions(+), 302 deletions(-) diff --git a/field_parser_v3_test.go b/field_parser_v3_test.go index 8b0b80cf4..da544b9b2 100644 --- a/field_parser_v3_test.go +++ b/field_parser_v3_test.go @@ -16,7 +16,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" example:"one"`, }}, @@ -28,7 +27,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" example:""`, }}, @@ -40,7 +38,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"float"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" example:"one"`, }}, @@ -55,7 +52,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" format:"csv"`, }}, @@ -69,7 +65,6 @@ func TestDefaultFieldParserV3(t *testing.T) { got, err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" binding:"required"`, }}, @@ -79,7 +74,6 @@ func TestDefaultFieldParserV3(t *testing.T) { got, err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required"`, }}, @@ -95,7 +89,6 @@ func TestDefaultFieldParserV3(t *testing.T) { &Parser{ RequiredByDefault: true, }, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test"`, }}, @@ -111,7 +104,6 @@ func TestDefaultFieldParserV3(t *testing.T) { &Parser{ RequiredByDefault: true, }, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" binding:"optional"`, }}, @@ -123,7 +115,6 @@ func TestDefaultFieldParserV3(t *testing.T) { &Parser{ RequiredByDefault: true, }, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"optional"`, }}, @@ -140,7 +131,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Extensions = map[string]interface{}{} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" extensions:"x-nullable,x-abc=def,!x-omitempty,x-example=[0, 9],x-example2={çãíœ, (bar=(abc, def)), [0,9]}"`, }}, @@ -160,7 +150,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" enums:"a,b,c"`, }}, @@ -172,7 +161,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"float"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" enums:"a,b,c"`, }}, @@ -189,7 +177,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Enum = []interface{}{} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" enums:"0,1,2" x-enum-varnames:"Daily,Weekly,Monthly"`, }}, @@ -201,7 +188,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"int"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" enums:"0,1,2,3" x-enum-varnames:"Daily,Weekly,Monthly"`, }}, @@ -218,7 +204,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Enum = []interface{}{} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" enums:"0,1,2" x-enum-varnames:"Daily,Weekly,Monthly"`, }}, @@ -235,7 +220,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" default:"pass"`, }}, @@ -247,7 +231,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"float"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" default:"pass"`, }}, @@ -262,7 +245,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maximum:"1"`, }}, @@ -275,7 +257,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maximum:"one"`, }}, @@ -286,7 +267,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"number"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maximum:"1"`, }}, @@ -299,7 +279,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"number"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maximum:"one"`, }}, @@ -310,7 +289,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"number"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" multipleOf:"1"`, }}, @@ -323,7 +301,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"number"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" multipleOf:"one"`, }}, @@ -334,7 +311,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" minimum:"1"`, }}, @@ -347,7 +323,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" minimum:"one"`, }}, @@ -362,7 +337,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maxLength:"1"`, }}, @@ -375,7 +349,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" maxLength:"one"`, }}, @@ -386,7 +359,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" minLength:"1"`, }}, @@ -399,7 +371,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" minLength:"one"`, }}, @@ -414,7 +385,6 @@ func TestDefaultFieldParserV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" readonly:"true"`, }}, @@ -422,24 +392,6 @@ func TestDefaultFieldParserV3(t *testing.T) { assert.NoError(t, err) assert.Equal(t, true, schema.Spec.ReadOnly) }) - - t.Run("OneOf tag", func(t *testing.T) { - t.Parallel() - - schema := spec.NewSchemaSpec() - schema.Spec.Type = []string{ANY} - err := newTagBaseFieldParserV3( - &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, - &ast.Field{Tag: &ast.BasicLit{ - Value: `json:"test" oneOf:"string,float64"`, - }}, - ).ComplementSchema(schema) - assert.NoError(t, err) - assert.Len(t, schema.Spec.OneOf, 2) - assert.Equal(t, spec.NewSingleOrArray("string"), schema.Spec.OneOf[0].Spec.Type) - assert.Equal(t, spec.NewSingleOrArray("number"), schema.Spec.OneOf[1].Spec.Type) - }) } func TestValidTagsV3(t *testing.T) { @@ -450,7 +402,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=10,min=1"`, }}, @@ -465,7 +416,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=10,gte=1"`, }}, @@ -478,7 +428,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=10,min=1"`, }}, @@ -497,7 +446,6 @@ func TestValidTagsV3(t *testing.T) { err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=10,min=1"`, }}, @@ -509,7 +457,6 @@ func TestValidTagsV3(t *testing.T) { // wrong validate tag will be ignored. err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=ten,min=1"`, }}, @@ -526,7 +473,6 @@ func TestValidTagsV3(t *testing.T) { err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof='red book' 'green book'"`, }}, @@ -538,7 +484,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof=1 2 3"`, }}, @@ -554,7 +499,6 @@ func TestValidTagsV3(t *testing.T) { err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof=red green yellow"`, }}, @@ -566,7 +510,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof='red green' blue 'c0x2Cc' 'd0x7Cd'"`, }}, @@ -578,7 +521,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof='c0x9Ab' book"`, }}, @@ -590,7 +532,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" binding:"oneof=foo bar" validate:"required,oneof=foo bar" enums:"a,b,c"`, }}, @@ -602,7 +543,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" binding:"oneof=aa bb" validate:"required,oneof=foo bar"`, }}, @@ -620,7 +560,6 @@ func TestValidTagsV3(t *testing.T) { err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,unique"`, }}, @@ -638,7 +577,6 @@ func TestValidTagsV3(t *testing.T) { err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,unique,max=10,min=1,oneof=a0x2Cc 'c0x7Cd book',omitempty,dive,max=1"`, }}, @@ -659,7 +597,6 @@ func TestValidTagsV3(t *testing.T) { err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof=,max=10=90,min=1"`, }}, @@ -675,7 +612,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Items.Schema.Spec.Type = []string{"string"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,max=10,min=one"`, }}, @@ -688,7 +624,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = []string{"integer"} err = newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" validate:"required,oneof=one two"`, }}, @@ -706,7 +641,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Items.Schema.Spec.Type = []string{"string"} err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" pattern:"^[a-zA-Z0-9_]*$"`, }}, @@ -722,7 +656,6 @@ func TestValidTagsV3(t *testing.T) { schema.Spec.Type = typeString err := newTagBaseFieldParserV3( &Parser{}, - &ast.File{Name: &ast.Ident{Name: "test"}}, &ast.Field{Tag: &ast.BasicLit{ Value: `json:"test" pattern:"^[a-zA-Z0-9_]*$"`, }}, diff --git a/field_parserv3.go b/field_parserv3.go index 37a9c646d..733907f4b 100644 --- a/field_parserv3.go +++ b/field_parserv3.go @@ -83,15 +83,13 @@ func (sf *structFieldV3) setMax(valValue string) { type tagBaseFieldParserV3 struct { p *Parser - file *ast.File field *ast.Field tag reflect.StructTag } -func newTagBaseFieldParserV3(p *Parser, file *ast.File, field *ast.Field) FieldParserV3 { +func newTagBaseFieldParserV3(p *Parser, field *ast.Field) FieldParserV3 { fieldParser := tagBaseFieldParserV3{ p: p, - file: file, field: field, tag: "", } @@ -136,10 +134,9 @@ func (ps *tagBaseFieldParserV3) ComplementSchema(schema *spec.RefOrSpec[spec.Sch if err != nil { return err } - if !reflect.ValueOf(newSchema).IsZero() { - newSchema.AllOf = []*spec.RefOrSpec[spec.Schema]{{Spec: schema.Spec}} - *schema = spec.RefOrSpec[spec.Schema]{Spec: &newSchema} - } + // if !reflect.ValueOf(newSchema).IsZero() { + // *schema = *(newSchema.WithAllOf(*schema.Spec)) + // } return nil } @@ -342,19 +339,6 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st } } - var oneOfSchemas []*spec.RefOrSpec[spec.Schema] - oneOfTagValue := ps.tag.Get(oneOfTag) - if oneOfTagValue != "" { - oneOfTypes := strings.Split((oneOfTagValue), ",") - for _, oneOfType := range oneOfTypes { - oneOfSchema, err := ps.p.getTypeSchemaV3(oneOfType, ps.file, true) - if err != nil { - return fmt.Errorf("can't find oneOf type %q: %v", oneOfType, err) - } - oneOfSchemas = append(oneOfSchemas, oneOfSchema) - } - } - elemSchema := schema if field.schemaType == ARRAY { @@ -378,7 +362,6 @@ func (ps *tagBaseFieldParserV3) complementSchema(schema *spec.Schema, types []st elemSchema.MinLength = field.minLength elemSchema.Enum = field.enums elemSchema.Pattern = field.pattern - elemSchema.OneOf = oneOfSchemas return nil } diff --git a/go.mod b/go.mod index 0ba74277f..8f6be0f9f 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.2 github.com/sv-tools/openapi v0.2.1 - golang.org/x/tools v0.13.0 + golang.org/x/tools v0.8.0 sigs.k8s.io/yaml v1.3.0 ) @@ -30,7 +30,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/urfave/cli/v2 v2.25.1 - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.7.0 // indirect gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 298a46f74..16966338a 100644 --- a/go.sum +++ b/go.sum @@ -64,11 +64,11 @@ github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/operation.go b/operation.go index 57850f27e..7569ee790 100644 --- a/operation.go +++ b/operation.go @@ -423,7 +423,6 @@ const ( extensionsTag = "extensions" collectionFormatTag = "collectionFormat" patternTag = "pattern" - oneOfTag = "oneOf" ) var regexAttributes = map[string]*regexp.Regexp{ diff --git a/operationv3.go b/operationv3.go index 503ef4653..4737694e1 100644 --- a/operationv3.go +++ b/operationv3.go @@ -5,7 +5,6 @@ import ( "fmt" "go/ast" "log" - "maps" "net/http" "strconv" "strings" @@ -927,15 +926,22 @@ func (o *OperationV3) ParseResponseComment(commentLine string, astFile *ast.File for _, codeStr := range strings.Split(matches[1], ",") { if strings.EqualFold(codeStr, defaultTag) { - codeStr = "" - } else { - code, err := strconv.Atoi(codeStr) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) - } - if description == "" { - description = http.StatusText(code) - } + response := o.DefaultResponse() + response.Description = description + + mimeType := "application/json" // TODO: set correct mimeType + setResponseSchema(response, mimeType, schema) + + continue + } + + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + if description == "" { + description = http.StatusText(code) } response := spec.NewResponseSpec() @@ -973,12 +979,15 @@ func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error { for _, codeStr := range strings.Split(matches[1], ",") { if strings.EqualFold(codeStr, defaultTag) { - codeStr = "" - } else { - _, err := strconv.Atoi(codeStr) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) - } + response := o.DefaultResponse() + response.Description = description + + continue + } + + _, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } o.AddResponse(codeStr, newResponseWithDescription(description)) @@ -987,10 +996,21 @@ func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error { return nil } +// DefaultResponse return the default response member pointer. +func (o *OperationV3) DefaultResponse() *spec.Response { + if o.Responses.Spec.Default == nil { + o.Responses.Spec.Default = spec.NewResponseSpec() + o.Responses.Spec.Default.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) + } + + if o.Responses.Spec.Default.Spec.Spec.Content == nil { + o.Responses.Spec.Default.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) + } + + return o.Responses.Spec.Default.Spec.Spec +} + // AddResponse add a response for a code. -// If the code is already exist, it will merge with the old one: -// 1. The description will be replaced by the new one if the new one is not empty. -// 2. The content schema will be merged using `oneOf` if the new one is not empty. func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Extendable[spec.Response]]) { if response.Spec.Spec.Headers == nil { response.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) @@ -1000,74 +1020,24 @@ func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Ext o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]]) } - res := response - var prev *spec.RefOrSpec[spec.Extendable[spec.Response]] - if code != "" { - prev = o.Responses.Spec.Response[code] - } else { - prev = o.Responses.Spec.Default - } - if prev != nil { // merge into prev - res = prev - if response.Spec.Spec.Description != "" { - prev.Spec.Spec.Description = response.Spec.Spec.Description - } - if len(response.Spec.Spec.Content) > 0 { - // responses should only have one content type - singleKey := "" - for k := range response.Spec.Spec.Content { - singleKey = k - break - } - if prevMediaType := prev.Spec.Spec.Content[singleKey]; prevMediaType == nil { - prev.Spec.Spec.Content = response.Spec.Spec.Content - } else { - newMediaType := response.Spec.Spec.Content[singleKey] - if len(newMediaType.Extensions) > 0 { - if prevMediaType.Extensions == nil { - prevMediaType.Extensions = make(map[string]interface{}) - } - maps.Copy(prevMediaType.Extensions, newMediaType.Extensions) - } - if len(newMediaType.Spec.Examples) > 0 { - if prevMediaType.Spec.Examples == nil { - prevMediaType.Spec.Examples = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Example]]) - } - maps.Copy(prevMediaType.Spec.Examples, newMediaType.Spec.Examples) - } - if prevSchema := prevMediaType.Spec.Schema; prevSchema.Ref != nil || prevSchema.Spec.OneOf == nil { - oneOfSchema := spec.NewSchemaSpec() - oneOfSchema.Spec.OneOf = []*spec.RefOrSpec[spec.Schema]{prevSchema, newMediaType.Spec.Schema} - prevMediaType.Spec.Schema = oneOfSchema - } else { - prevSchema.Spec.OneOf = append(prevSchema.Spec.OneOf, newMediaType.Spec.Schema) - } - } - } - } - - if code != "" { - o.Responses.Spec.Response[code] = res - } else { - o.Responses.Spec.Default = res - } + o.Responses.Spec.Response[code] = response } // ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200. func (o *OperationV3) ParseEmptyResponseOnly(commentLine string) error { for _, codeStr := range strings.Split(commentLine, ",") { - var description string if strings.EqualFold(codeStr, defaultTag) { - codeStr = "" - } else { - code, err := strconv.Atoi(codeStr) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) - } - description = http.StatusText(code) + _ = o.DefaultResponse() + + continue } - o.AddResponse(codeStr, newResponseWithDescription(description)) + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + + o.AddResponse(codeStr, newResponseWithDescription(http.StatusText(code))) } return nil diff --git a/parserv3.go b/parserv3.go index 33aef2c31..d7953e555 100644 --- a/parserv3.go +++ b/parserv3.go @@ -15,8 +15,8 @@ import ( "github.com/sv-tools/openapi/spec" ) -// FieldParserFactoryV3 create FieldParser. -type FieldParserFactoryV3 func(ps *Parser, file *ast.File, field *ast.Field) FieldParserV3 +// FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 create FieldParser. +type FieldParserFactoryV3 func(ps *Parser, field *ast.Field) FieldParserV3 // FieldParserV3 parse struct field. type FieldParserV3 interface { @@ -903,7 +903,7 @@ func (p *Parser) parseStructFieldV3(file *ast.File, field *ast.Field) (map[strin } } - ps := p.fieldParserFactoryV3(p, file, field) + ps := p.fieldParserFactoryV3(p, field) if ps.ShouldSkip() { return nil, nil, nil diff --git a/parserv3_test.go b/parserv3_test.go index af56f04cd..3123f25f3 100644 --- a/parserv3_test.go +++ b/parserv3_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/sv-tools/openapi/spec" ) func TestOverridesGetTypeSchemaV3(t *testing.T) { @@ -359,6 +358,7 @@ func TestParseSimpleApiV3(t *testing.T) { assert.NoError(t, err) paths := p.openAPI.Paths.Spec.Paths + assert.Equal(t, 15, len(paths)) path := paths["/testapi/get-string-by-int/{some_id}"].Spec.Spec.Get.Spec assert.Equal(t, "get string by ID", path.Description) @@ -372,98 +372,6 @@ func TestParseSimpleApiV3(t *testing.T) { assert.NotNil(t, path) assert.NotNil(t, path.RequestBody) //TODO add asserts - - t.Run("Test parse struct oneOf", func(t *testing.T) { - t.Parallel() - - assert.Contains(t, p.openAPI.Components.Spec.Schemas, "web.OneOfTest") - schema := p.openAPI.Components.Spec.Schemas["web.OneOfTest"].Spec - expected := `{ - "properties": { - "big_int": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - }, - "pet_detail": { - "oneOf": [ - { - "$ref": "#/components/schemas/web.Cat" - }, - { - "$ref": "#/components/schemas/web.Dog" - } - ] - } - }, - "type": "object" -}` - out, err := json.MarshalIndent(schema, "", " ") - assert.NoError(t, err) - assert.Equal(t, expected, string(out)) - - assert.Contains(t, p.openAPI.Components.Spec.Schemas, "web.Cat") - schema = p.openAPI.Components.Spec.Schemas["web.Cat"].Spec - expected = `{ - "properties": { - "age": { - "type": "integer" - }, - "hunts": { - "type": "boolean" - } - }, - "type": "object" -}` - out, err = json.MarshalIndent(schema, "", " ") - assert.NoError(t, err) - assert.Equal(t, expected, string(out)) - - assert.Contains(t, p.openAPI.Components.Spec.Schemas, "web.Dog") - schema = p.openAPI.Components.Spec.Schemas["web.Dog"].Spec - expected = `{ - "properties": { - "bark": { - "type": "boolean" - }, - "breed": { - "enum": [ - "Dingo", - "Husky", - "Retriever", - "Shepherd" - ], - "type": "string" - } - }, - "type": "object" -}` - out, err = json.MarshalIndent(schema, "", " ") - assert.NoError(t, err) - assert.Equal(t, expected, string(out)) - }) - - t.Run("Test parse response oneOf", func(t *testing.T) { - t.Parallel() - - assert.Contains(t, paths, "/pets/{id}") - path := paths["/pets/{id}"] - assert.Contains(t, path.Spec.Spec.Get.Spec.Responses.Spec.Response, "200") - response = path.Spec.Spec.Get.Spec.Responses.Spec.Response["200"] - assert.Equal(t, "Return Cat or Dog", response.Spec.Spec.Description) - mediaType := response.Spec.Spec.Content["application/json"] - rootSchema := mediaType.Spec.Schema.Spec - assert.Equal(t, []*spec.RefOrSpec[spec.Schema]{ - {Ref: &spec.Ref{Ref: "#/components/schemas/web.Cat"}}, - {Ref: &spec.Ref{Ref: "#/components/schemas/web.Dog"}}, - }, rootSchema.OneOf) - - }) } func TestParserParseServers(t *testing.T) { diff --git a/testdata/v3/simple/api/api.go b/testdata/v3/simple/api/api.go index 39ca8bad6..91433b466 100644 --- a/testdata/v3/simple/api/api.go +++ b/testdata/v3/simple/api/api.go @@ -144,19 +144,3 @@ func GetPet6FunctionScopedResponse() { func FormData() { } - -// @Success 200 {object} web.OneOfTest -// @Router /GetOneOfTypes [get] -func GetOneOfTypes() { - -} - -// @Summary Get pet by ID -// @Param id path string true "ID" -// @Success 200 {object} web.Cat -// @Success 200 {object} web.Dog -// @Success 200 "Return Cat or Dog" -// @Router /pets/{id} [get] -func GetPetByID() { - -} diff --git a/testdata/v3/simple/web/handler.go b/testdata/v3/simple/web/handler.go index a8e03ac9d..3afcdd0d1 100644 --- a/testdata/v3/simple/web/handler.go +++ b/testdata/v3/simple/web/handler.go @@ -112,18 +112,3 @@ type Request struct { Password string `json:"password"` RefreshToken string `json:"refresh_token"` } - -type OneOfTest struct { - BigInt any `json:"big_int" oneOf:"string,int64"` - PetDetail any `json:"pet_detail" oneOf:"Cat,Dog"` -} - -type Dog struct { - Bark bool `json:"bark"` - Breed string `json:"breed" enums:"Dingo,Husky,Retriever,Shepherd"` -} - -type Cat struct { - Hunts bool `json:"hunts"` - Age int `json:"age"` -} From 5a199fcd5070bd1c6a42b091f50b02a46eabd0f5 Mon Sep 17 00:00:00 2001 From: Jerry Wang Date: Fri, 18 Oct 2024 06:03:21 +0800 Subject: [PATCH 33/34] feat: add securitydefinitions.bearerauth for bearer token setting (#1854) --- README.md | 86 +++++++++++++++++++++++++++------------------ parser.go | 1 + parserv3.go | 9 ++++- parserv3_test.go | 6 +++- schema.go | 1 + testdata/v3/main.go | 2 ++ 6 files changed, 69 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 69e963187..4b9d72136 100644 --- a/README.md +++ b/README.md @@ -17,34 +17,51 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a variety of plugins for popular [Go web frameworks](#supported-web-frameworks). This allows you to quickly integrate with an existing Go project (using Swagger UI). ## Contents - - [Getting started](#getting-started) - - [Supported Web Frameworks](#supported-web-frameworks) - - [How to use it with Gin](#how-to-use-it-with-gin) - - [The swag formatter](#the-swag-formatter) - - [Implementation Status](#implementation-status) - - [Declarative Comments Format](#declarative-comments-format) - - [General API Info](#general-api-info) - - [API Operation](#api-operation) - - [Security](#security) - - [Examples](#examples) - - [Descriptions over multiple lines](#descriptions-over-multiple-lines) - - [User defined structure with an array type](#user-defined-structure-with-an-array-type) - - [Function scoped struct declaration](#function-scoped-struct-declaration) - - [Model composition in response](#model-composition-in-response) - - [Add a headers in response](#add-a-headers-in-response) - - [Use multiple path params](#use-multiple-path-params) - - [Example value of struct](#example-value-of-struct) - - [SchemaExample of body](#schemaexample-of-body) - - [Description of struct](#description-of-struct) - - [Use swaggertype tag to supported custom type](#use-swaggertype-tag-to-supported-custom-type) - - [Use global overrides to support a custom type](#use-global-overrides-to-support-a-custom-type) - - [Use swaggerignore tag to exclude a field](#use-swaggerignore-tag-to-exclude-a-field) - - [Add extension info to struct field](#add-extension-info-to-struct-field) - - [Rename model to display](#rename-model-to-display) - - [How to use security annotations](#how-to-use-security-annotations) - - [Add a description for enum items](#add-a-description-for-enum-items) - - [Generate only specific docs file types](#generate-only-specific-docs-file-types) -- [About the Project](#about-the-project) +- [swag](#swag) + - [Contents](#contents) + - [Getting started](#getting-started) + - [swag cli](#swag-cli) + - [Supported Web Frameworks](#supported-web-frameworks) + - [How to use it with Gin](#how-to-use-it-with-gin) + - [The swag formatter](#the-swag-formatter) + - [Implementation Status](#implementation-status) +- [Declarative Comments Format](#declarative-comments-format) + - [General API Info](#general-api-info) + - [Using markdown descriptions](#using-markdown-descriptions) + - [Open API V3.1.0+](#open-api-v310) + - [API Operation](#api-operation) + - [Mime Types](#mime-types) + - [Param Type](#param-type) + - [Data Type](#data-type) + - [Security](#security) + - [Attribute](#attribute) + - [Available](#available) + - [Future](#future) + - [Examples](#examples) + - [Descriptions over multiple lines](#descriptions-over-multiple-lines) + - [User defined structure with an array type](#user-defined-structure-with-an-array-type) + - [Function scoped struct declaration](#function-scoped-struct-declaration) + - [Model composition in response](#model-composition-in-response) + - [Add a headers in response](#add-a-headers-in-response) + - [Use multiple path params](#use-multiple-path-params) + - [Add multiple paths](#add-multiple-paths) + - [Example value of struct](#example-value-of-struct) + - [SchemaExample of body](#schemaexample-of-body) + - [Description of struct](#description-of-struct) + - [Use swaggertype tag to supported custom type](#use-swaggertype-tag-to-supported-custom-type) + - [Use global overrides to support a custom type](#use-global-overrides-to-support-a-custom-type) + - [Use swaggerignore tag to exclude a field](#use-swaggerignore-tag-to-exclude-a-field) + - [Add extension info to struct field](#add-extension-info-to-struct-field) + - [Rename model to display](#rename-model-to-display) + - [How to use security annotations](#how-to-use-security-annotations) + - [Add a description for enum items](#add-a-description-for-enum-items) + - [Generate only specific docs file types](#generate-only-specific-docs-file-types) + - [Change the default Go Template action delimiters](#change-the-default-go-template-action-delimiters) + - [About the Project](#about-the-project) + - [Contributors](#contributors) + - [Backers](#backers) + - [Sponsors](#sponsors) + - [License](#license) ## Getting started @@ -486,12 +503,13 @@ Besides that, `swag` also accepts aliases for some MIME Types as follows: ## Security | annotation | description | parameters | example | |------------|-------------|------------|---------| -| securitydefinitions.basic | [Basic](https://swagger.io/docs/specification/2-0/authentication/basic-authentication/) auth. | | // @securityDefinitions.basic BasicAuth | -| securitydefinitions.apikey | [API key](https://swagger.io/docs/specification/2-0/authentication/api-keys/) auth. | in, name, description | // @securityDefinitions.apikey ApiKeyAuth | -| securitydefinitions.oauth2.application | [OAuth2 application](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope, description | // @securitydefinitions.oauth2.application OAuth2Application | -| securitydefinitions.oauth2.implicit | [OAuth2 implicit](https://swagger.io/docs/specification/authentication/oauth2/) auth. | authorizationUrl, scope, description | // @securitydefinitions.oauth2.implicit OAuth2Implicit | -| securitydefinitions.oauth2.password | [OAuth2 password](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope, description | // @securitydefinitions.oauth2.password OAuth2Password | -| securitydefinitions.oauth2.accessCode | [OAuth2 access code](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, authorizationUrl, scope, description | // @securitydefinitions.oauth2.accessCode OAuth2AccessCode | +| securitydefinitions.basic | [Basic](https://swagger.io/docs/specification/2-0/authentication/basic-authentication/) auth. | | // @securityDefinitions.basic BasicAuth | +| securitydefinitions.apikey | [API key](https://swagger.io/docs/specification/2-0/authentication/api-keys/) auth. | in, name, description | // @securityDefinitions.apikey ApiKeyAuth | +| securitydefinitions.oauth2.application | [OAuth2 application](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope, description | // @securitydefinitions.oauth2.application OAuth2Application | +| securitydefinitions.oauth2.implicit | [OAuth2 implicit](https://swagger.io/docs/specification/authentication/oauth2/) auth. | authorizationUrl, scope, description | // @securitydefinitions.oauth2.implicit OAuth2Implicit | +| securitydefinitions.oauth2.password | [OAuth2 password](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope, description | // @securitydefinitions.oauth2.password OAuth2Password | +| securitydefinitions.oauth2.accessCode | [OAuth2 access code](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, authorizationUrl, scope, description | // @securitydefinitions.oauth2.accessCode OAuth2AccessCode | +| securitydefinitions.bearerauth | [Bearer Authentication](https://swagger.io/docs/specification/authentication/bearer-authentication/) auth. supported in Swagger v3.x| | // @securitydefinitions.bearerauth BearerAuth | | parameters annotation | example | diff --git a/parser.go b/parser.go index 0f92110eb..d37f64ebb 100644 --- a/parser.go +++ b/parser.go @@ -59,6 +59,7 @@ const ( descriptionMarkdownAttr = "@description.markdown" secBasicAttr = "@securitydefinitions.basic" secAPIKeyAttr = "@securitydefinitions.apikey" + secBearerAuthAttr = "@securitydefinitions.bearerauth" secApplicationAttr = "@securitydefinitions.oauth2.application" secImplicitAttr = "@securitydefinitions.oauth2.implicit" secPasswordAttr = "@securitydefinitions.oauth2.password" diff --git a/parserv3.go b/parserv3.go index d7953e555..14766f877 100644 --- a/parserv3.go +++ b/parserv3.go @@ -128,7 +128,7 @@ func (p *Parser) parseGeneralAPIInfoV3(comments []string) error { } tag.Spec.ExternalDocs.Spec.Description = value - case secBasicAttr, secAPIKeyAttr, secApplicationAttr, secImplicitAttr, secPasswordAttr, secAccessCodeAttr: + case secBasicAttr, secAPIKeyAttr, secApplicationAttr, secImplicitAttr, secPasswordAttr, secAccessCodeAttr, secBearerAuthAttr: key, scheme, err := parseSecAttributesV3(attribute, comments, &line) if err != nil { return err @@ -351,6 +351,13 @@ func parseSecAttributesV3(context string, lines []string, index *int) (string, * search = []string{authorizationURL, in} case secAccessCodeAttr: search = []string{tokenURL, authorizationURL, in} + case secBearerAuthAttr: + scheme := spec.SecurityScheme{ + Type: "http", + Scheme: "bearer", + BearerFormat: "JWT", + } + return "bearerauth", &scheme, nil } // For the first line we get the attributes in the context parameter, so we skip to the next one diff --git a/parserv3_test.go b/parserv3_test.go index 3123f25f3..57e3afa81 100644 --- a/parserv3_test.go +++ b/parserv3_test.go @@ -136,7 +136,7 @@ func TestParserParseGeneralApiInfoV3(t *testing.T) { assert.Equal(t, "OpenAPI", p.openAPI.ExternalDocs.Spec.Description) assert.Equal(t, "https://swagger.io/resources/open-api", p.openAPI.ExternalDocs.Spec.URL) - assert.Equal(t, 6, len(p.openAPI.Components.Spec.SecuritySchemes)) + assert.Equal(t, 7, len(p.openAPI.Components.Spec.SecuritySchemes)) security := p.openAPI.Components.Spec.SecuritySchemes assert.Equal(t, "basic", security["basic"].Spec.Spec.Scheme) @@ -164,6 +164,10 @@ func TestParserParseGeneralApiInfoV3(t *testing.T) { assert.Equal(t, "oauth2", security["OAuth2AccessCode"].Spec.Spec.Type) assert.Equal(t, "header", security["OAuth2AccessCode"].Spec.Spec.In) assert.Equal(t, "https://example.com/oauth/token", security["OAuth2AccessCode"].Spec.Spec.Flows.Spec.AuthorizationCode.Spec.TokenURL) + + assert.Equal(t, "bearer", security["bearerauth"].Spec.Spec.Scheme) + assert.Equal(t, "http", security["bearerauth"].Spec.Spec.Type) + assert.Equal(t, "JWT", security["bearerauth"].Spec.Spec.BearerFormat) } func TestParser_ParseGeneralApiInfoExtensionsV3(t *testing.T) { diff --git a/schema.go b/schema.go index b3a5b38c1..c78a43528 100644 --- a/schema.go +++ b/schema.go @@ -3,6 +3,7 @@ package swag import ( "errors" "fmt" + "github.com/go-openapi/spec" ) diff --git a/testdata/v3/main.go b/testdata/v3/main.go index de93578c1..305d369e3 100644 --- a/testdata/v3/main.go +++ b/testdata/v3/main.go @@ -54,6 +54,8 @@ package main // @in header // @name name +// @securitydefinitions.bearerauth BearerAuth + // @externalDocs.description OpenAPI // @externalDocs.url https://swagger.io/resources/open-api From 415a688832cd13a3f6543995dc830e273ac8d22a Mon Sep 17 00:00:00 2001 From: Jerry Wang Date: Fri, 18 Oct 2024 06:34:21 +0800 Subject: [PATCH 34/34] docs: modify cli readme for v2 (#1850) --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b9d72136..f8f0640fa 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a varie 2. Download swag by using: ```sh -go install github.com/swaggo/swag/cmd/swag@latest +go install github.com/swaggo/swag/v2/cmd/swag@latest ``` To build from source you need [Go](https://golang.org/dl/) (1.18 or newer). @@ -95,6 +95,7 @@ swag init ```sh swag init -h +Swag version: v2.0.0 NAME: swag init - Create docs.go @@ -120,10 +121,13 @@ OPTIONS: --instanceName value This parameter can be used to name different swagger document instances. It is optional. --overridesFile value File to read global type overrides from. (default: ".swaggo") --parseGoList Parse dependency via 'go list' (default: true) + --parseExtension value Parse only those operations that match given extension --tags value, -t value A comma-separated list of tags to filter the APIs for which the documentation is generated.Special case if the tag is prefixed with the '!' character then the APIs with that tag will be excluded + --v3.1 Generate OpenAPI V3.1 spec (default: false) --templateDelims value, --td value Provide custom delimeters for Go template generation. The format is leftDelim,rightDelim. For example: "[[,]]" + --packageName --output A package name of docs.go, using output directory name by default (check --output option) --collectionFormat value, --cf value Set default collection format (default: "csv") - --help, -h show help (default: false) + --help, -h show help ``` ```bash