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"