diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4abd3fc28..ef9d51a7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ jobs: test: strategy: matrix: - go: [ '1.11.x', '1.12.x' ] + go: [ '1.13.x', '1.14.x' ] platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: @@ -37,4 +37,4 @@ jobs: working-directory: ./src/github.com/${{ github.repository }} run: make test env: - GOPATH: ${{ runner.workspace }} \ No newline at end of file + GOPATH: ${{ runner.workspace }} diff --git a/.gitignore b/.gitignore index bea8db681..a77cb497c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,5 @@ cover.out # Etc .DS_Store -swag -swag.exe +/swag +/swag.exe diff --git a/.goreleaser.yml b/.goreleaser.yml index d0b576ab1..fcae4cd4a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,5 +1,15 @@ build: main: cmd/swag/main.go + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - 386 + ignore: + - goos: darwin + goarch: arm64 archive: replacements: darwin: Darwin @@ -7,6 +17,7 @@ archive: windows: Windows 386: i386 amd64: x86_64 + arm64: aarch64 checksum: name_template: 'checksums.txt' snapshot: diff --git a/.travis.yml b/.travis.yml index 825e2482f..daadd9cdd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: go go: - - 1.11.x - - 1.12.x - 1.13.x + - 1.14.x + - 1.15.x install: - make deps diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..0a5d9b8db --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Dockerfile References: https://docs.docker.com/engine/reference/builder/ + +# Start from the latest golang base image +FROM golang:1.14-alpine as builder + +# Set the Current Working Directory inside the container +WORKDIR /app + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed +RUN go mod download + +# Copy the source from the current directory to the Working Directory inside the container +COPY . . + +# Build the Go app +RUN CGO_ENABLED=0 GOOS=linux go build -v -a -installsuffix cgo -o swag cmd/swag/main.go + + +######## Start a new stage from scratch ####### +FROM scratch + +WORKDIR /root/ + +# Copy the Pre-built binary file from the previous stage +COPY --from=builder /app/swag . + diff --git a/Makefile b/Makefile index f78299612..2731f56d8 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,6 @@ clean: deps: $(GOGET) github.com/swaggo/cli $(GOGET) github.com/ghodss/yaml - $(GOGET) github.com/gin-gonic/gin $(GOGET) github.com/KyleBanks/depth $(GOGET) github.com/go-openapi/jsonreference $(GOGET) github.com/go-openapi/spec diff --git a/README.md b/README.md index 9d987c265..1ddc6a78c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # swag +🌍 *[English](README.md) ∙ [简体中文](README_zh-CN.md)* + [![Travis Status](https://img.shields.io/travis/swaggo/swag/master.svg)](https://travis-ci.org/swaggo/swag) @@ -26,12 +28,15 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a varie - [Examples](#examples) - [Descriptions over multiple lines](#descriptions-over-multiple-lines) - [User defined structure with an array type](#user-defined-structure-with-an-array-type) + - [Model composition in response](#model-composition-in-response) - [Add a headers in response](#add-a-headers-in-response) - [Use multiple path params](#use-multiple-path-params) - [Example value of struct](#example-value-of-struct) - [Description of struct](#description-of-struct) - [Use swaggertype tag to supported custom type](#use-swaggertype-tag-to-supported-custom-type) + - [Use swaggerignore tag to exclude a field](#use-swaggerignore-tag-to-exclude-a-field) - [Add extension info to struct field](#add-extension-info-to-struct-field) + - [Rename model to display](#rename-model-to-display) - [How to using security annotations](#how-to-using-security-annotations) - [About the Project](#about-the-project) @@ -68,12 +73,19 @@ USAGE: swag init [command options] [arguments...] OPTIONS: - --generalInfo value, -g value Go file path in which 'swagger general API Info' is written (default: "main.go") - --dir value, -d value Directory you want to parse (default: "./") - --propertyStrategy value, -p value Property Naming Strategy like snakecase,camelcase,pascalcase (default: "camelcase") - --output value, -o value Output directory for all the generated files(swagger.json, swagger.yaml and doc.go) (default: "./docs") - --parseVendor Parse go files in 'vendor' folder, disabled by default - --parseDependency Parse go files in outside dependency folder, disabled by default + --generalInfo value, -g value Go file path in which 'swagger general API Info' is written (default: "main.go") + --dir value, -d value Directory you want to parse (default: "./") + --exclude value Exclude directories and files when searching, comma separated + --propertyStrategy value, -p value Property Naming Strategy like snakecase,camelcase,pascalcase (default: "camelcase") + --output value, -o value Output directory for all the generated files(swagger.json, swagger.yaml and doc.go) (default: "./docs") + --parseVendor Parse go files in 'vendor' folder, disabled by default (default: false) + --parseDependency Parse go files in outside dependency folder, disabled by default (default: false) + --markdownFiles value, --md value Parse folder containing markdown files to use as description, disabled by default + --codeExampleFiles value, --cef value Parse folder containing code example files to use for the x-codeSamples extension, disabled by default + --parseInternal Parse go files in internal packages, disabled by default (default: false) + --generatedTime Generate timestamp at the top of docs.go, disabled by default (default: false) + --parseDepth value Dependency parse depth (default: 100) + --help, -h show help (default: false) ``` ## Supported Web Frameworks @@ -82,6 +94,9 @@ OPTIONS: - [echo](http://github.com/swaggo/echo-swagger) - [buffalo](https://github.com/swaggo/buffalo-swagger) - [net/http](https://github.com/swaggo/http-swagger) +- [flamingo](https://github.com/i-love-flamingo/swagger) +- [fiber](https://github.com/arsmn/fiber-swagger) +- [atreugo](https://github.com/Nerzal/atreugo-swagger) ## How to use it with Gin @@ -110,6 +125,7 @@ import "github.com/swaggo/files" // swagger embed files // @host localhost:8080 // @BasePath /api/v1 +// @query.collection.format multi // @securityDefinitions.basic BasicAuth @@ -164,7 +180,7 @@ func main() { //... ``` -Additionally some general API info can be set dynamically. The generated code package `docs` exports `SwaggerInfo` variable which we can use to set the title, description, version, host and base path programatically. Example using Gin: +Additionally some general API info can be set dynamically. The generated code package `docs` exports `SwaggerInfo` variable which we can use to set the title, description, version, host and base path programmatically. Example using Gin: ```go package main @@ -188,7 +204,7 @@ import ( func main() { - // programatically set swagger info + // programmatically set swagger info docs.SwaggerInfo.Title = "Swagger Example API" docs.SwaggerInfo.Description = "This is a sample server Petstore server." docs.SwaggerInfo.Version = "1.0" @@ -229,9 +245,9 @@ import ( // @Param id path int true "Account ID" // @Success 200 {object} model.Account // @Header 200 {string} Token "qwerty" -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError +// @Failure 400,404 {object} httputil.HTTPError // @Failure 500 {object} httputil.HTTPError +// @Failure default {object} httputil.DefaultError // @Router /accounts/{id} [get] func (c *Controller) ShowAccount(ctx *gin.Context) { id := ctx.Param("id") @@ -256,9 +272,9 @@ func (c *Controller) ShowAccount(ctx *gin.Context) { // @Param q query string false "name search by q" // @Success 200 {array} model.Account // @Header 200 {string} Token "qwerty" -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError +// @Failure 400,404 {object} httputil.HTTPError // @Failure 500 {object} httputil.HTTPError +// @Failure default {object} httputil.DefaultError // @Router /accounts [get] func (c *Controller) ListAccounts(ctx *gin.Context) { q := ctx.Request.URL.Query().Get("q") @@ -325,6 +341,7 @@ $ swag init | license.url | A URL to the license used for the API. MUST be in the format of a URL. | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html | | host | The host (name or ip) serving the API. | // @host localhost:8080 | | BasePath | The base path on which the API is served. | // @BasePath /api/v1 | +| query.collection.format | The default collection(array) param format in query,enums:csv,multi,pipes,tsv,ssv. If not set, csv is the default.| // @query.collection.format multi | schemes | The transfer protocol for the operation that separated by spaces. | // @schemes http https | | x-name | The extension key, must be start by x- and take only json value | // @x-example-key {"key": "value"} | @@ -341,7 +358,6 @@ When a short string in your documentation is insufficient, or you need images, c | tag.description.markdown | Description of the tag this is an alternative to tag.description. The description will be read from a file named like tagname.md | // @tag.description.markdown | - ## API Operation **Example** @@ -351,6 +367,7 @@ When a short string in your documentation is insufficient, or you need images, c | annotation | description | |-------------|----------------------------------------------------------------------------------------------------------------------------| | description | A verbose explanation of the operation behavior. | +| description.markdown | A short description of the application. The description will be read from a file named like endpointname.md| // @description.file endpoint.description.markdown | | id | A unique string used to identify the operation. Must be unique among all API operations. | | tags | A list of tags to each API operation that separated by commas. | | summary | A short summary of what the operation does. | @@ -358,11 +375,16 @@ When a short string in your documentation is insufficient, or you need images, c | produce | A list of MIME types the APIs can produce. Value MUST be as described under [Mime Types](#mime-types). | | param | Parameters that separated by spaces. `param name`,`param type`,`data type`,`is mandatory?`,`comment` `attribute(optional)` | | security | [Security](#security) to each API operation. | -| success | Success response that separated by spaces. `return code`,`{param type}`,`data type`,`comment` | -| failure | Failure response that separated by spaces. `return code`,`{param type}`,`data type`,`comment` | +| success | Success response that separated by spaces. `return code or default`,`{param type}`,`data type`,`comment` | +| failure | Failure response that separated by spaces. `return code or default`,`{param type}`,`data type`,`comment` | +| response | As same as `success` and `failure` | | header | Header in response that separated by spaces. `return code`,`{param type}`,`data type`,`comment` | | router | Path definition that separated by spaces. `path`,`[httpMethod]` | | x-name | The extension key, must be start by x- and take only json value. | +| x-codeSample | Optional Markdown usage. take `file` as parameter. This will then search for a file named like the summary in the given folder. | +| deprecated | Mark endpoint as deprecated. | + + ## Mime Types @@ -429,8 +451,9 @@ Besides that, `swag` also accepts aliases for some MIME Types as follows: // @Param enumint query int false "int enums" Enums(1, 2, 3) // @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) // @Param string query string false "string valid" minlength(5) maxlength(10) -// @Param int query int false "int valid" mininum(1) maxinum(10) +// @Param int query int false "int valid" minimum(1) maximum(10) // @Param default query string false "string default" default(A) +// @Param collection query []string false "string collection" collectionFormat(multi) ``` It also works for the struct fields: @@ -447,6 +470,7 @@ type Foo struct { Field Name | Type | Description ---|:---:|--- +validate | `string` | Determines the validation for the parameter. Possible values are: `required`. default | * | Declares the value of the parameter that the server will use if none is provided, for example a "count" to control the number of results per page might default to 100 if not supplied by the client in the request. (Note: "default" has no meaning for required parameters.) See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6.2. Unlike JSON Schema this value MUST conform to the defined [`type`](#parameterType) for this parameter. maximum | `number` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.2. minimum | `number` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.3. @@ -454,6 +478,7 @@ Field Name | Type | Description minLength | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.2. enums | [\*] | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.1. format | `string` | The extending format for the previously mentioned [`type`](#parameterType). See [Data Type Formats](https://swagger.io/specification/v2/#dataTypeFormat) for further details. +collectionFormat | `string` |Determines the format of the array if type array is used. Possible values are: Default value is `csv`. ### Future @@ -464,7 +489,6 @@ Field Name | Type | Description maxItems | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.2. minItems | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.3. uniqueItems | `boolean` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.4. -collectionFormat | `string` | Determines the format of the array if type array is used. Possible values are: Default value is `csv`. ## Examples @@ -492,12 +516,53 @@ type Account struct { Name string `json:"name" example:"account name"` } ``` + +### Model composition in response +```go +// JSONResult's data field will be overridden by the specific type proto.Order +@success 200 {object} jsonresult.JSONResult{data=proto.Order} "desc" +``` + +```go +type JSONResult struct { + Code int `json:"code" ` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +type Order struct { //in `proto` package + Id uint `json:"id"` + Data interface{} `json:"data"` +} +``` + +- also support array of objects and primitive types as nested response +```go +@success 200 {object} jsonresult.JSONResult{data=[]proto.Order} "desc" +@success 200 {object} jsonresult.JSONResult{data=string} "desc" +@success 200 {object} jsonresult.JSONResult{data=[]string} "desc" +``` + +- overriding multiple fields. field will be added if not exists +```go +@success 200 {object} jsonresult.JSONResult{data1=string,data2=[]string,data3=proto.Order,data4=[]proto.Order} "desc" +``` +- overriding deep-level fields +```go +type DeepObject struct { //in `proto` package + ... +} +@success 200 {object} jsonresult.JSONResult{data1=proto.Order{data=proto.DeepObject},data2=[]proto.Order{data=[]proto.DeepObject}} "desc" +``` ### Add a headers in response ```go // @Success 200 {string} string "ok" +// @failure 400 {string} string "error" +// @response default {string} string "other error" // @Header 200 {string} Location "/entity/1" -// @Header 200 {string} Token "qwerty" +// @Header 200,400,default {string} Token "token" +// @Header all {string} Token2 "token2" ``` ### Use multiple path params @@ -594,6 +659,17 @@ generated swagger doc as follows: ``` + +### Use swaggerignore tag to exclude a field + +```go +type Account struct { + ID string `json:"id"` + Name string `json:"name"` + Ignored int `swaggerignore:"true"` +} +``` + ### Add extension info to struct field ```go @@ -616,6 +692,13 @@ generate swagger doc as follows: } } ``` +### Rename model to display + +```golang +type Resp struct { + Code int +}//@name Response +``` ### How to using security annotations diff --git a/README_zh-CN.md b/README_zh-CN.md new file mode 100644 index 000000000..fc8edac0e --- /dev/null +++ b/README_zh-CN.md @@ -0,0 +1,744 @@ +# swag + +🌍 *[English](README.md) ∙ [简体中文](README_zh-CN.md)* + + + +[![Travis Status](https://img.shields.io/travis/swaggo/swag/master.svg)](https://travis-ci.org/swaggo/swag) +[![Coverage Status](https://img.shields.io/codecov/c/github/swaggo/swag/master.svg)](https://codecov.io/gh/swaggo/swag) +[![Go Report Card](https://goreportcard.com/badge/github.com/swaggo/swag)](https://goreportcard.com/report/github.com/swaggo/swag) +[![codebeat badge](https://codebeat.co/badges/71e2f5e5-9e6b-405d-baf9-7cc8b5037330)](https://codebeat.co/projects/github.aaakk.us.kg-swaggo-swag-master) +[![Go Doc](https://godoc.org/github.com/swaggo/swagg?status.svg)](https://godoc.org/github.com/swaggo/swag) +[![Backers on Open Collective](https://opencollective.com/swag/backers/badge.svg)](#backers) +[![Sponsors on Open Collective](https://opencollective.com/swag/sponsors/badge.svg)](#sponsors) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fswaggo%2Fswag.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fswaggo%2Fswag?ref=badge_shield) +[![Release](https://img.shields.io/github/release/swaggo/swag.svg?style=flat-square)](https://github.com/swaggo/swag/releases) + +Swag将Go的注释转换为Swagger2.0文档。我们为流行的 [Go Web Framework](#支持的Web框架) 创建了各种插件,这样可以与现有Go项目快速集成(使用Swagger UI)。 + +## 目录 + +- [快速开始](#快速开始) +- [支持的Web框架](#支持的web框架) +- [如何与Gin集成](#如何与gin集成) +- [开发现状](#开发现状) +- [声明式注释格式](#声明式注释格式) + - [通用API信息](#通用api信息) + - [API操作](#api操作) + - [安全性](#安全性) +- [样例](#样例) + - [多行的描述](#多行的描述) + - [用户自定义的具有数组类型的结构](#用户自定义的具有数组类型的结构) + - [响应对象中的模型组合](#响应对象中的模型组合) + - [在响应中增加头字段](#在响应中增加头字段) + - [使用多路径参数](#使用多路径参数) + - [结构体的示例值](#结构体的示例值) + - [结构体描述](#结构体描述) + - [使用`swaggertype`标签更改字段类型](#使用`swaggertype`标签更改字段类型) + - [使用`swaggerignore`标签排除字段](#使用swaggerignore标签排除字段) + - [将扩展信息添加到结构字段](#将扩展信息添加到结构字段) + - [对展示的模型重命名](#对展示的模型重命名) + - [如何使用安全性注释](#如何使用安全性注释) +- [项目相关](#项目相关) + +## 快速开始 + +1. 将注释添加到API源代码中,请参阅声明性注释格式。 +2. 使用如下命令下载swag: + +```bash +go get -u github.com/swaggo/swag/cmd/swag +``` + +从源码开始构建的话,需要有Go环境(1.9及以上版本)。 + +或者从github的release页面下载预编译好的二进制文件。 + +3. 在包含`main.go`文件的项目根目录运行`swag init`。这将会解析注释并生成需要的文件(`docs`文件夹和`docs/docs.go`)。 + +```bash +swag init +``` + +确保导入了生成的`docs/docs.go`文件,这样特定的配置文件才会被初始化。如果通用API指数没有写在`main.go`中,可以使用`-g`标识符来告知swag。 + +```bash +swag init -g http/api.go +``` + +## swag cli + +```bash +swag init -h +NAME: + swag init - Create docs.go + +USAGE: + swag init [command options] [arguments...] + +OPTIONS: + --generalInfo value, -g value API通用信息所在的go源文件路径,如果是相对路径则基于API解析目录 (默认: "main.go") + --dir value, -d value API解析目录 (默认: "./") + --propertyStrategy value, -p value 结构体字段命名规则,三种:snakecase,camelcase,pascalcase (默认: "camelcase") + --output value, -o value 文件(swagger.json, swagger.yaml and doc.go)输出目录 (默认: "./docs") + --parseVendor 是否解析vendor目录里的go源文件,默认不 + --parseDependency 是否解析依赖目录中的go源文件,默认不 + --markdownFiles value, --md value 指定API的描述信息所使用的markdown文件所在的目录 + --generatedTime 是否输出时间到输出文件docs.go的顶部,默认是 +``` + +## 支持的Web框架 + +- [gin](http://github.com/swaggo/gin-swagger) +- [echo](http://github.com/swaggo/echo-swagger) +- [buffalo](https://github.com/swaggo/buffalo-swagger) +- [net/http](https://github.com/swaggo/http-swagger) + +## 如何与Gin集成 + +[点击此处](https://github.com/swaggo/swag/tree/master/example/celler)查看示例源代码。 + +1. 使用`swag init`生成Swagger2.0文档后,导入如下代码包: + +```go +import "github.com/swaggo/gin-swagger" // gin-swagger middleware +import "github.com/swaggo/files" // swagger embed files +``` + +2. 在`main.go`源代码中添加通用的API注释: + +```go +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server celler 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 localhost:8080 +// @BasePath /api/v1 +// @query.collection.format multi + +// @securityDefinitions.basic BasicAuth + +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name Authorization + +// @securitydefinitions.oauth2.application OAuth2Application +// @tokenUrl https://example.com/oauth/token +// @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 +// @scope.write Grants write access +// @scope.admin Grants read and write access to administrative information + +// @securitydefinitions.oauth2.password OAuth2Password +// @tokenUrl https://example.com/oauth/token +// @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-extension-openapi {"example": "value on a json format"} + +func main() { + r := gin.Default() + + c := controller.NewController() + + v1 := r.Group("/api/v1") + { + accounts := v1.Group("/accounts") + { + accounts.GET(":id", c.ShowAccount) + accounts.GET("", c.ListAccounts) + accounts.POST("", c.AddAccount) + accounts.DELETE(":id", c.DeleteAccount) + accounts.PATCH(":id", c.UpdateAccount) + accounts.POST(":id/images", c.UploadAccountImage) + } + //... + } + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + r.Run(":8080") +} +//... +``` + +此外,可以动态设置一些通用的API信息。生成的代码包`docs`导出`SwaggerInfo`变量,使用该变量可以通过编码的方式设置标题、描述、版本、主机和基础路径。使用Gin的示例: + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/swaggo/files" + "github.com/swaggo/gin-swagger" + + "./docs" // docs is generated by Swag CLI, you have to import it. +) + +// @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 + +// @termsOfService http://swagger.io/terms/ + +func main() { + + // programatically set swagger info + docs.SwaggerInfo.Title = "Swagger Example API" + docs.SwaggerInfo.Description = "This is a sample server Petstore server." + docs.SwaggerInfo.Version = "1.0" + docs.SwaggerInfo.Host = "petstore.swagger.io" + docs.SwaggerInfo.BasePath = "/v2" + docs.SwaggerInfo.Schemes = []string{"http", "https"} + + r := gin.New() + + // use ginSwagger middleware to serve the API docs + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + r.Run() +} +``` + +3. 在`controller`代码中添加API操作注释: + +```go +package controller + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/swaggo/swag/example/celler/httputil" + "github.com/swaggo/swag/example/celler/model" +) + +// ShowAccount godoc +// @Summary Show a account +// @Description get string by ID +// @ID get-string-by-int +// @Accept json +// @Produce json +// @Param id path int true "Account ID" +// @Success 200 {object} model.Account +// @Header 200 {string} Token "qwerty" +// @Failure 400,404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Failure default {object} httputil.DefaultError +// @Router /accounts/{id} [get] +func (c *Controller) ShowAccount(ctx *gin.Context) { + id := ctx.Param("id") + aid, err := strconv.Atoi(id) + if err != nil { + httputil.NewError(ctx, http.StatusBadRequest, err) + return + } + account, err := model.AccountOne(aid) + if err != nil { + httputil.NewError(ctx, http.StatusNotFound, err) + return + } + ctx.JSON(http.StatusOK, account) +} + +// ListAccounts godoc +// @Summary List accounts +// @Description get accounts +// @Accept json +// @Produce json +// @Param q query string false "name search by q" +// @Success 200 {array} model.Account +// @Header 200 {string} Token "qwerty" +// @Failure 400,404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Failure default {object} httputil.DefaultError +// @Router /accounts [get] +func (c *Controller) ListAccounts(ctx *gin.Context) { + q := ctx.Request.URL.Query().Get("q") + accounts, err := model.AccountsAll(q) + if err != nil { + httputil.NewError(ctx, http.StatusNotFound, err) + return + } + ctx.JSON(http.StatusOK, accounts) +} + +//... +``` + +```bash +swag init +``` + +4. 运行程序,然后在浏览器中访问 http://localhost:8080/swagger/index.html。将看到Swagger 2.0 Api文档,如下所示: + +![swagger_index.html](https://raw.githubusercontent.com/swaggo/swag/master/assets/swagger-image.png) + +## 开发现状 + +[Swagger 2.0 文档](https://swagger.io/docs/specification/2-0/basic-structure/) + +- [x] Basic Structure +- [x] API Host and Base Path +- [x] Paths and Operations +- [x] Describing Parameters +- [x] Describing Request Body +- [x] Describing Responses +- [x] MIME Types +- [x] Authentication + - [x] Basic Authentication + - [x] API Keys +- [x] Adding Examples +- [x] File Upload +- [x] Enums +- [x] Grouping Operations With Tags +- [ ] Swagger Extensions + +## 声明式注释格式 + +## 通用API信息 + +**示例** [`celler/main.go`](https://github.com/swaggo/swag/blob/master/example/celler/main.go) + +| 注释 | 说明 | 示例 | +| ----------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| title | **必填** 应用程序的名称。 | // @title Swagger Example API | +| version | **必填** 提供应用程序API的版本。 | // @version 1.0 | +| description | 应用程序的简短描述。 | // @description This is a sample server celler server. | +| tag.name | 标签的名称。 | // @tag.name This is the name of the tag | +| tag.description | 标签的描述。 | // @tag.description Cool Description | +| tag.docs.url | 标签的外部文档的URL。 | // @tag.docs.url https://example.com | +| tag.docs.description | 标签的外部文档说明。 | // @tag.docs.description Best example documentation | +| termsOfService | API的服务条款。 | // @termsOfService http://swagger.io/terms/ | +| contact.name | 公开的API的联系信息。 | // @contact.name API Support | +| contact.url | 联系信息的URL。 必须采用网址格式。 | // @contact.url http://www.swagger.io/support | +| contact.email | 联系人/组织的电子邮件地址。 必须采用电子邮件地址的格式。 | // @contact.email support@swagger.io | +| license.name | **必填** 用于API的许可证名称。 | // @license.name Apache 2.0 | +| license.url | 用于API的许可证的URL。 必须采用网址格式。 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html | +| host | 运行API的主机(主机名或IP地址)。 | // @host localhost:8080 | +| BasePath | 运行API的基本路径。 | // @BasePath /api/v1 | +| query.collection.format | 请求URI query里数组参数的默认格式:csv,multi,pipes,tsv,ssv。 如果未设置,则默认为csv。 | // @query.collection.format multi | +| schemes | 用空格分隔的请求的传输协议。 | // @schemes http https | +| x-name | 扩展的键必须以x-开头,并且只能使用json值 | // @x-example-key {"key": "value"} | + +### 使用Markdown描述 + +如果文档中的短字符串不足以完整表达,或者需要展示图片,代码示例等类似的内容,则可能需要使用Markdown描述。要使用Markdown描述,请使用一下注释。 + +| 注释 | 说明 | 示例 | +| ------------------------ | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | +| title | **必填** 应用程序的名称。 | // @title Swagger Example API | +| version | **必填** 提供应用程序API的版本。 | // @version 1.0 | +| description.markdown | 应用程序的简短描述。 从`api.md`文件中解析。 这是`@description`的替代用法。 | // @description.markdown No value needed, this parses the description from api.md | +| tag.name | 标签的名称。 | // @tag.name This is the name of the tag | +| tag.description.markdown | 标签说明,这是`tag.description`的替代用法。 该描述将从名为`tagname.md的`文件中读取。 | // @tag.description.markdown | + +## API操作 + +Example [celler/controller](https://github.com/swaggo/swag/tree/master/example/celler/controller) + +| 注释 | 描述 | +| -------------------- | ------------------------------------------------------------------------------------------------------- | +| description | 操作行为的详细说明。 | +| description.markdown | 应用程序的简短描述。该描述将从名为`endpointname.md`的文件中读取。 | +| id | 用于标识操作的唯一字符串。在所有API操作中必须唯一。 | +| tags | 每个API操作的标签列表,以逗号分隔。 | +| summary | 该操作的简短摘要。 | +| accept | API可以使用的MIME类型的列表。值必须如“[Mime类型](#mime-types)”中所述。 | +| produce | API可以生成的MIME类型的列表。值必须如“[Mime类型](#mime-types)”中所述。 | +| param | 用空格分隔的参数。`param name`,`param type`,`data type`,`is mandatory?`,`comment` `attribute(optional)` | +| security | 每个API操作的[安全性](#security)。 | +| success | 以空格分隔的成功响应。`return code`,`{param type}`,`data type`,`comment` | +| failure | 以空格分隔的故障响应。`return code`,`{param type}`,`data type`,`comment` | +| response | 与success、failure作用相同 | +| header | 以空格分隔的头字段。 `return code`,`{param type}`,`data type`,`comment` | +| router | 以空格分隔的路径定义。 `path`,`[httpMethod]` | +| x-name | 扩展字段必须以`x-`开头,并且只能使用json值。 | + +## Mime类型 + +`swag` 接受所有格式正确的MIME类型, 即使匹配 `*/*`。除此之外,`swag`还接受某些MIME类型的别名,如下所示: + +| Alias | MIME Type | +| --------------------- | --------------------------------- | +| json | application/json | +| xml | text/xml | +| plain | text/plain | +| html | text/html | +| mpfd | multipart/form-data | +| x-www-form-urlencoded | application/x-www-form-urlencoded | +| json-api | application/vnd.api+json | +| json-stream | application/x-json-stream | +| octet-stream | application/octet-stream | +| png | image/png | +| jpeg | image/jpeg | +| gif | image/gif | + +## 参数类型 + +- query +- path +- header +- body +- formData + +## 数据类型 + +- string (string) +- integer (int, uint, uint32, uint64) +- number (float32) +- boolean (bool) +- user defined struct + +## 安全性 + +| 注释 | 描述 | 参数 | 示例 | +| -------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------ | +| securitydefinitions.basic | [Basic](https://swagger.io/docs/specification/2-0/authentication/basic-authentication/) auth. | | // @securityDefinitions.basic BasicAuth | +| securitydefinitions.apikey | [API key](https://swagger.io/docs/specification/2-0/authentication/api-keys/) auth. | in, name | // @securityDefinitions.apikey ApiKeyAuth | +| securitydefinitions.oauth2.application | [OAuth2 application](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope | // @securitydefinitions.oauth2.application OAuth2Application | +| securitydefinitions.oauth2.implicit | [OAuth2 implicit](https://swagger.io/docs/specification/authentication/oauth2/) auth. | authorizationUrl, scope | // @securitydefinitions.oauth2.implicit OAuth2Implicit | +| securitydefinitions.oauth2.password | [OAuth2 password](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, scope | // @securitydefinitions.oauth2.password OAuth2Password | +| securitydefinitions.oauth2.accessCode | [OAuth2 access code](https://swagger.io/docs/specification/authentication/oauth2/) auth. | tokenUrl, authorizationUrl, scope | // @securitydefinitions.oauth2.accessCode OAuth2AccessCode | + +| 参数注释 | 示例 | +| ---------------- | -------------------------------------------------------- | +| in | // @in header | +| name | // @name Authorization | +| tokenUrl | // @tokenUrl https://example.com/oauth/token | +| authorizationurl | // @authorizationurl https://example.com/oauth/authorize | +| scope.hoge | // @scope.write Grants write access | + +## 属性 + +```go +// @Param enumstring query string false "string enums" Enums(A, B, C) +// @Param enumint query int false "int enums" Enums(1, 2, 3) +// @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) +// @Param string query string false "string valid" minlength(5) maxlength(10) +// @Param int query int false "int valid" minimum(1) maximum(10) +// @Param default query string false "string default" default(A) +// @Param collection query []string false "string collection" collectionFormat(multi) +``` + +也适用于结构体字段: + +```go +type Foo struct { + Bar string `minLength:"4" maxLength:"16"` + Baz int `minimum:"10" maximum:"20" default:"15"` + Qux []string `enums:"foo,bar,baz"` +} +``` + +### 当前可用的 + +| 字段名 | 类型 | 描述 | +| ---------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| default | * | 声明如果未提供任何参数,则服务器将使用的默认参数值,例如,如果请求中的客户端未提供该参数,则用于控制每页结果数的“计数”可能默认为100。 (注意:“default”对于必需的参数没有意义)。参看 https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6.2。 与JSON模式不同,此值务必符合此参数的定义[类型](#parameterType)。 | +| maximum | `number` | 参看 https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.2. | +| minimum | `number` | 参看 https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.3. | +| maxLength | `integer` | 参看 https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.1. | +| minLength | `integer` | 参看 https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.2. | +| enums | [\*] | 参看 https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.1. | +| format | `string` | 上面提到的[类型](#parameterType)的扩展格式。有关更多详细信息,请参见[数据类型格式](https://swagger.io/specification/v2/#dataTypeFormat)。 | +| collectionFormat | `string` | 指定query数组参数的格式。 可能的值为: 默认值是 `csv`。 | + +### 进一步的 + +| 字段名 | 类型 | 描述 | +| ----------- | :-------: | ---------------------------------------------------------------------------------- | +| multipleOf | `number` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.1.1. | +| pattern | `string` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.3. | +| maxItems | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.2. | +| minItems | `integer` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.3. | +| uniqueItems | `boolean` | See https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.4. | + +## 样例 + +### 多行的描述 + +可以在常规api描述或路由定义中添加跨越多行的描述,如下所示: + +```go +// @description This is the first line +// @description This is the second line +// @description And so forth. +``` + +### 用户自定义的具有数组类型的结构 + +```go +// @Success 200 {array} model.Account <-- This is a user defined struct. +``` + +```go +package model + +type Account struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"account name"` +} +``` + +### 响应对象中的模型组合 + +```go +// JSONResult的data字段类型将被proto.Order类型替换 +@success 200 {object} jsonresult.JSONResult{data=proto.Order} "desc" +``` + +```go +type JSONResult struct { + Code int `json:"code" ` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +type Order struct { //in `proto` package + ... +} +``` + +- 还支持对象数组和原始类型作为嵌套响应 + +```go +@success 200 {object} jsonresult.JSONResult{data=[]proto.Order} "desc" +@success 200 {object} jsonresult.JSONResult{data=string} "desc" +@success 200 {object} jsonresult.JSONResult{data=[]string} "desc" +``` + +- 替换多个字段的类型。如果某字段不存在,将添加该字段。 + +```go +@success 200 {object} jsonresult.JSONResult{data1=string,data2=[]string,data3=proto.Order,data4=[]proto.Order} "desc" +``` + +### 在响应中增加头字段 + +```go +// @Success 200 {string} string "ok" +// @failure 400 {string} string "error" +// @response default {string} string "other error" +// @Header 200 {string} Location "/entity/1" +// @Header 200,400,default {string} Token "token" +// @Header all {string} Token2 "token2" +``` + +### 使用多路径参数 + +```go +/// ... +// @Param group_id path int true "Group ID" +// @Param account_id path int true "Account ID" +// ... +// @Router /examples/groups/{group_id}/accounts/{account_id} [get] +``` + +### 结构体的示例值 + +```go +type Account struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"account name"` + PhotoUrls []string `json:"photo_urls" example:"http://test/image/1.jpg,http://test/image/2.jpg"` +} +``` + +### 结构体描述 + +```go +type Account struct { + // ID this is userid + ID int `json:"id"` + Name string `json:"name"` // This is Name +} +``` + +### 使用`swaggertype`标签更改字段类型 + +[#201](https://github.com/swaggo/swag/issues/201#issuecomment-475479409) + +```go +type TimestampTime struct { + time.Time +} + +///实现encoding.JSON.Marshaler接口 +func (t *TimestampTime) MarshalJSON() ([]byte, error) { + bin := make([]byte, 16) + bin = strconv.AppendInt(bin[:0], t.Time.Unix(), 10) + return bin, nil +} + +///实现encoding.JSON.Unmarshaler接口 +func (t *TimestampTime) UnmarshalJSON(bin []byte) error { + v, err := strconv.ParseInt(string(bin), 10, 64) + if err != nil { + return err + } + t.Time = time.Unix(v, 0) + return nil +} +/// + +type Account struct { + // 使用`swaggertype`标签将别名类型更改为内置类型integer + ID sql.NullInt64 `json:"id" swaggertype:"integer"` + + // 使用`swaggertype`标签更改struct类型为内置类型integer + RegisterTime TimestampTime `json:"register_time" swaggertype:"primitive,integer"` + + // Array types can be overridden using "array," format + Coeffs []big.Float `json:"coeffs" swaggertype:"array,number"` +} +``` + +[#379](https://github.com/swaggo/swag/issues/379) + +```go +type CerticateKeyPair struct { + Crt []byte `json:"crt" swaggertype:"string" format:"base64" example:"U3dhZ2dlciByb2Nrcw=="` + Key []byte `json:"key" swaggertype:"string" format:"base64" example:"U3dhZ2dlciByb2Nrcw=="` +} +``` + +生成的swagger文档如下: + +```go +"api.MyBinding": { + "type":"object", + "properties":{ + "crt":{ + "type":"string", + "format":"base64", + "example":"U3dhZ2dlciByb2Nrcw==" + }, + "key":{ + "type":"string", + "format":"base64", + "example":"U3dhZ2dlciByb2Nrcw==" + } + } +} +``` + +### 使用`swaggerignore`标签排除字段 + +```go +type Account struct { + ID string `json:"id"` + Name string `json:"name"` + Ignored int `swaggerignore:"true"` +} +``` + +### 将扩展信息添加到结构字段 + +```go +type Account struct { + ID string `json:"id" extensions:"x-nullable,x-abc=def"` // 扩展字段必须以"x-"开头 +} +``` + +生成swagger文档,如下所示: + +```go +"Account": { + "type": "object", + "properties": { + "id": { + "type": "string", + "x-nullable": true, + "x-abc": "def" + } + } +} +``` + +### 对展示的模型重命名 + +```go +type Resp struct { + Code int +}//@name Response +``` + +### 如何使用安全性注释 + +通用API信息。 + +```go +// @securityDefinitions.basic BasicAuth + +// @securitydefinitions.oauth2.application OAuth2Application +// @tokenUrl https://example.com/oauth/token +// @scope.write Grants write access +// @scope.admin Grants read and write access to administrative information +``` + +每个API操作。 + +```go +// @Security ApiKeyAuth +``` + +使用AND条件。 + +```go +// @Security ApiKeyAuth +// @Security OAuth2Application[write, admin] +``` + +## 项目相关 + +This project was inspired by [yvasiyarov/swagger](https://github.com/yvasiyarov/swagger) but we simplified the usage and added support a variety of [web frameworks](#supported-web-frameworks). Gopher image source is [tenntenn/gopher-stickers](https://github.com/tenntenn/gopher-stickers). It has licenses [creative commons licensing](http://creativecommons.org/licenses/by/3.0/deed.en). + +## 贡献者 + +This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. + + +## 支持者 + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/swag#backer)] + + + +## 赞助商 + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/swag#sponsor)] + + + + + + + + + + + + +## License + +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fswaggo%2Fswag.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fswaggo%2Fswag?ref=badge_large) diff --git a/cmd/swag/main.go b/cmd/swag/main.go index 59dd0d4a8..75c175617 100644 --- a/cmd/swag/main.go +++ b/cmd/swag/main.go @@ -12,35 +12,47 @@ import ( const ( searchDirFlag = "dir" + excludeFlag = "exclude" generalInfoFlag = "generalInfo" propertyStrategyFlag = "propertyStrategy" outputFlag = "output" parseVendorFlag = "parseVendor" parseDependencyFlag = "parseDependency" markdownFilesFlag = "markdownFiles" + codeExampleFilesFlag = "codeExampleFiles" + parseInternalFlag = "parseInternal" generatedTimeFlag = "generatedTime" + parseDepthFlag = "parseDepth" ) var initFlags = []cli.Flag{ &cli.StringFlag{ - Name: generalInfoFlag + ", g", - Value: "main.go", - Usage: "Go file path in which 'swagger general API Info' is written", + Name: generalInfoFlag, + Aliases: []string{"g"}, + Value: "main.go", + Usage: "Go file path in which 'swagger general API Info' is written", }, &cli.StringFlag{ - Name: searchDirFlag + ", d", - Value: "./", - Usage: "Directory you want to parse", + Name: searchDirFlag, + Aliases: []string{"d"}, + Value: "./", + Usage: "Directory you want to parse", }, &cli.StringFlag{ - Name: propertyStrategyFlag + ", p", - Value: "camelcase", - Usage: "Property Naming Strategy like snakecase,camelcase,pascalcase", + Name: excludeFlag, + Usage: "Exclude directories and files when searching, comma separated", }, &cli.StringFlag{ - Name: outputFlag + ", o", - Value: "./docs", - Usage: "Output directory for all the generated files(swagger.json, swagger.yaml and doc.go)", + Name: propertyStrategyFlag, + Aliases: []string{"p"}, + Value: "camelcase", + Usage: "Property Naming Strategy like snakecase,camelcase,pascalcase", + }, + &cli.StringFlag{ + Name: outputFlag, + Aliases: []string{"o"}, + Value: "./docs", + Usage: "Output directory for all the generated files(swagger.json, swagger.yaml and doc.go)", }, &cli.BoolFlag{ Name: parseVendorFlag, @@ -51,13 +63,29 @@ var initFlags = []cli.Flag{ Usage: "Parse go files in outside dependency folder, disabled by default", }, &cli.StringFlag{ - Name: markdownFilesFlag + ", md", - Value: "", - Usage: "Parse folder containing markdown files to use as description, disabled by default", + Name: markdownFilesFlag, + Aliases: []string{"md"}, + Value: "", + Usage: "Parse folder containing markdown files to use as description, disabled by default", + }, + &cli.StringFlag{ + Name: codeExampleFilesFlag, + Aliases: []string{"cef"}, + Value: "", + Usage: "Parse folder containing code example files to use for the x-codeSamples extension, disabled by default", }, &cli.BoolFlag{ - Name: "generatedTime", - Usage: "Generate timestamp at the top of docs.go, true by default", + Name: parseInternalFlag, + Usage: "Parse go files in internal packages, disabled by default", + }, + &cli.BoolFlag{ + Name: generatedTimeFlag, + Usage: "Generate timestamp at the top of docs.go, disabled by default", + }, + &cli.IntFlag{ + Name: parseDepthFlag, + Value: 100, + Usage: "Dependency parse depth", }, } @@ -71,14 +99,18 @@ func initAction(c *cli.Context) error { } return gen.New().Build(&gen.Config{ - SearchDir: c.String(searchDirFlag), - MainAPIFile: c.String(generalInfoFlag), - PropNamingStrategy: strategy, - OutputDir: c.String(outputFlag), - ParseVendor: c.Bool(parseVendorFlag), - ParseDependency: c.Bool(parseDependencyFlag), - MarkdownFilesDir: c.String(markdownFilesFlag), - GeneratedTime: c.Bool(generatedTimeFlag), + SearchDir: c.String(searchDirFlag), + Excludes: c.String(excludeFlag), + MainAPIFile: c.String(generalInfoFlag), + PropNamingStrategy: strategy, + OutputDir: c.String(outputFlag), + ParseVendor: c.Bool(parseVendorFlag), + ParseDependency: c.Bool(parseDependencyFlag), + MarkdownFilesDir: c.String(markdownFilesFlag), + ParseInternal: c.Bool(parseInternalFlag), + GeneratedTime: c.Bool(generatedTimeFlag), + CodeExampleFilesDir: c.String(codeExampleFilesFlag), + ParseDepth: c.Int(parseDepthFlag), }) } diff --git a/example/basic/api/api.go b/example/basic/api/api.go index 3709ff5d3..52be29cd5 100644 --- a/example/basic/api/api.go +++ b/example/basic/api/api.go @@ -1,7 +1,9 @@ package api import ( - "github.com/gin-gonic/gin" + "encoding/json" + "net/http" + "github.com/swaggo/swag/example/basic/web" ) @@ -17,9 +19,9 @@ import ( // @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(c *gin.Context) { +func GetStringByInt(w http.ResponseWriter, r *http.Request) { var pet web.Pet - if err := c.ShouldBindJSON(&pet); err != nil { + if err := json.NewDecoder(r.Body).Decode(&pet); err != nil { // write your code return } @@ -39,7 +41,7 @@ func GetStringByInt(c *gin.Context) { // @Failure 400 {object} web.APIError "We need ID!!" // @Failure 404 {object} web.APIError "Can not find ID" // @Router /testapi/get-struct-array-by-string/{some_id} [get] -func GetStructArrayByString(c *gin.Context) { +func GetStructArrayByString(w http.ResponseWriter, r *http.Request) { // write your code } @@ -54,7 +56,7 @@ func GetStructArrayByString(c *gin.Context) { // @Failure 400 {object} web.APIError "We need ID!!" // @Failure 404 {object} web.APIError "Can not find ID" // @Router /file/upload [post] -func Upload(ctx *gin.Context) { +func Upload(w http.ResponseWriter, r *http.Request) { // write your code } diff --git a/example/basic/main.go b/example/basic/main.go index a93ea32c3..9f22eb586 100644 --- a/example/basic/main.go +++ b/example/basic/main.go @@ -1,7 +1,8 @@ package main import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/example/basic/api" ) @@ -20,10 +21,8 @@ import ( // @host petstore.swagger.io // @BasePath /v2 func main() { - r := gin.New() - r.GET("/testapi/get-string-by-int/:some_id", api.GetStringByInt) - r.GET("//testapi/get-struct-array-by-string/:some_id", api.GetStructArrayByString) - r.POST("/testapi/upload", api.Upload) - r.Run() - + 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/example/celler/controller/accounts.go b/example/celler/controller/accounts.go index 72d4d46dc..0c0b0709d 100644 --- a/example/celler/controller/accounts.go +++ b/example/celler/controller/accounts.go @@ -11,7 +11,7 @@ import ( ) // ShowAccount godoc -// @Summary Show a account +// @Summary Show an account // @Description get string by ID // @Tags accounts // @Accept json @@ -60,7 +60,7 @@ func (c *Controller) ListAccounts(ctx *gin.Context) { } // AddAccount godoc -// @Summary Add a account +// @Summary Add an account // @Description add by json account // @Tags accounts // @Accept json @@ -94,7 +94,7 @@ func (c *Controller) AddAccount(ctx *gin.Context) { } // UpdateAccount godoc -// @Summary Update a account +// @Summary Update an account // @Description Update by json account // @Tags accounts // @Accept json @@ -131,7 +131,7 @@ func (c *Controller) UpdateAccount(ctx *gin.Context) { } // DeleteAccount godoc -// @Summary Update a account +// @Summary Delete an account // @Description Delete by account ID // @Tags accounts // @Accept json diff --git a/example/celler/controller/examples.go b/example/celler/controller/examples.go index 5670ec8d0..d3838f88d 100644 --- a/example/celler/controller/examples.go +++ b/example/celler/controller/examples.go @@ -22,7 +22,6 @@ import ( // @Router /examples/ping [get] func (c *Controller) PingExample(ctx *gin.Context) { ctx.String(http.StatusOK, "pong") - return } // CalcExample godoc @@ -33,7 +32,7 @@ func (c *Controller) PingExample(ctx *gin.Context) { // @Produce json // @Param val1 query int true "used for calc" // @Param val2 query int true "used for calc" -// @Success 200 {integer} integer "answer" +// @Success 200 {integer} string "answer" // @Failure 400 {string} string "ok" // @Failure 404 {string} string "ok" // @Failure 500 {string} string "ok" @@ -123,7 +122,7 @@ func (c *Controller) SecuritiesExample(ctx *gin.Context) { // @Param enumint query int false "int enums" Enums(1, 2, 3) // @Param enumnumber query number false "int enums" Enums(1.1, 1.2, 1.3) // @Param string query string false "string valid" minlength(5) maxlength(10) -// @Param int query int false "int valid" mininum(1) maxinum(10) +// @Param int query int false "int valid" minimum(1) maximum(10) // @Param default query string false "string default" default(A) // @Success 200 {string} string "answer" // @Failure 400 {string} string "ok" @@ -140,3 +139,15 @@ func (c *Controller) AttributeExample(ctx *gin.Context) { ctx.Query("default"), )) } + +// PostExample godoc +// @Summary post request example +// @Description post request example +// @Accept json +// @Produce plain +// @Param message body model.Account true "Account Info" +// @Success 200 {string} string "success" +// @Failure 500 {string} string "fail" +// @Router /examples/post [post] +func (c *Controller) PostExample(ctx *gin.Context) { +} diff --git a/example/celler/go.mod b/example/celler/go.mod new file mode 100644 index 000000000..72c8d24e9 --- /dev/null +++ b/example/celler/go.mod @@ -0,0 +1,11 @@ +module github.com/swaggo/swag/example/celler + +go 1.13 + +require ( + github.com/gin-gonic/gin v1.6.3 + github.com/gofrs/uuid v3.3.0+incompatible + github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 + github.com/swaggo/gin-swagger v1.3.0 + golang.org/x/net v0.0.0-20201110031124-69a78807bb2b +) diff --git a/example/celler/go.sum b/example/celler/go.sum new file mode 100644 index 000000000..dc2785804 --- /dev/null +++ b/example/celler/go.sum @@ -0,0 +1,163 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +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/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.0 h1:BqWKpV1dFd+AuiKlgtddwVIFQsuMpxfBDBHGfM2yNpk= +github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4= +github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.9/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28= +github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gofrs/uuid v1.2.0 h1:coDhrjgyJaglxSjxuJdqQSSdUpG3w6p1OwN2od6frBU= +github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +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/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +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/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 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +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/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= +github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= +github.com/swaggo/gin-swagger v1.3.0 h1:eOmp7r57oUgZPw2dJOjcGNMse9cvXcI4tTqBcnZtPsI= +github.com/swaggo/gin-swagger v1.3.0/go.mod h1:oy1BRA6WvgtCp848lhxce7BnWH4C8Bxa0m5SkWx+cS0= +github.com/swaggo/swag v1.5.1 h1:2Agm8I4K5qb00620mHq0VJ05/KT4FtmALPIcQR9lEZM= +github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= +github.com/swaggo/swag v1.6.2 h1:WQMAtT/FmMBb7g0rAuHDhG3vvdtHKJ3WZ+Ssb0p4Y6E= +github.com/swaggo/swag v1.6.2/go.mod h1:YyZstMc22WYm6GEDx/CYWxq+faBbjQ5EqwQcrjREDBo= +github.com/swaggo/swag v1.6.9 h1:BukKRwZjnEcUxQt7Xgfrt9fpav0hiWw9YimdNO9wssw= +github.com/swaggo/swag v1.6.9/go.mod h1:a0IpNeMfGidNOcm2TsqODUh9JHdHu3kxDA0UlGbBKjI= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= +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 v1.1.13 h1:nB3O5kBSQGjEQAcfe1aLUYuxmXdFKmYgBZhY32rQb6Q= +github.com/ugorji/go v1.1.13/go.mod h1:jxau1n+/wyTGLQoCkjok9r5zFa/FxT6eI5HiHKQszjc= +github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +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 v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b h1:/mJ+GKieZA6hFDQGdWZrjj4AXPl5ylY+5HusG80roy0= +golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +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-20191204190536-9bdfabe68543/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= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/example/celler/model/account.go b/example/celler/model/account.go index a3c7427fe..54fb9b5d6 100644 --- a/example/celler/model/account.go +++ b/example/celler/model/account.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - uuid "github.com/satori/go.uuid" + uuid "github.com/gofrs/uuid" ) // Account example diff --git a/example/markdown/api/api.go b/example/markdown/api/api.go index 2bc900e27..db115f7d3 100644 --- a/example/markdown/api/api.go +++ b/example/markdown/api/api.go @@ -7,7 +7,7 @@ import ( // User example type User struct { - Id int64 + ID int64 Email string Password string } @@ -15,8 +15,8 @@ type User struct { // UsersCollection example type UsersCollection []User -// APIError example -type APIError struct { +// Error example +type Error struct { ErrorCode int ErrorMessage string CreatedAt time.Time diff --git a/example/object-map-example/controller/api.go b/example/object-map-example/controller/api.go new file mode 100644 index 000000000..83b5e99fe --- /dev/null +++ b/example/object-map-example/controller/api.go @@ -0,0 +1,25 @@ +package controller + +import "github.com/gin-gonic/gin" + +// GetMap godoc +// @Summary Get Map Example +// @Description get map +// @ID get-map +// @Accept json +// @Produce json +// @Success 200 {object} Response +// @Router /test [get] +func (c *Controller) GetMap(ctx *gin.Context) { + ctx.JSON(200, Response{ + Title: map[string]string{ + "en": "Map", + }, + CustomType: map[string]interface{}{ + "key": "value", + }, + Object: Data{ + Text: "object text", + }, + }) +} diff --git a/example/object-map-example/controller/controller.go b/example/object-map-example/controller/controller.go new file mode 100644 index 000000000..735d745de --- /dev/null +++ b/example/object-map-example/controller/controller.go @@ -0,0 +1,10 @@ +package controller + +// Controller example +type Controller struct { +} + +// NewController example +func NewController() *Controller { + return &Controller{} +} diff --git a/example/object-map-example/controller/response.go b/example/object-map-example/controller/response.go new file mode 100644 index 000000000..d0692f08c --- /dev/null +++ b/example/object-map-example/controller/response.go @@ -0,0 +1,11 @@ +package controller + +type Response struct { + Title map[string]string `json:"title" example:"en:Map,ru:Карта,kk:Карталар"` + CustomType map[string]interface{} `json:"map_data" swaggertype:"object,string" example:"key:value,key2:value2"` + Object Data `json:"object"` +} + +type Data struct { + Text string `json:"title" example:"Object data"` +} diff --git a/example/object-map-example/docs/docs.go b/example/object-map-example/docs/docs.go new file mode 100644 index 000000000..f73e708a4 --- /dev/null +++ b/example/object-map-example/docs/docs.go @@ -0,0 +1,141 @@ +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag + +package docs + +import ( + "bytes" + "encoding/json" + "strings" + + "github.com/alecthomas/template" + "github.com/swaggo/swag" +) + +var doc = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{.Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": {}, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/test": { + "get": { + "description": "get map", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Get Map Example", + "operationId": "get-map", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + } + }, + "definitions": { + "controller.Data": { + "type": "object", + "properties": { + "title": { + "type": "string", + "example": "Object data" + } + } + }, + "controller.Response": { + "type": "object", + "properties": { + "map_data": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "key": "value", + "key2": "value2" + } + }, + "object": { + "$ref": "#/definitions/controller.Data" + }, + "title": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "en": "Map", + "kk": "Карталар", + "ru": "Карта" + } + } + } + } + } +}` + +type swaggerInfo struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string +} + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = swaggerInfo{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "Swagger Map Example API", + Description: "", +} + +type s struct{} + +func (s *s) ReadDoc() string { + sInfo := SwaggerInfo + sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) + + t, err := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + }).Parse(doc) + if err != nil { + return doc + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, sInfo); err != nil { + return doc + } + + return tpl.String() +} + +func init() { + swag.Register(swag.Name, &s{}) +} diff --git a/example/object-map-example/docs/swagger.json b/example/object-map-example/docs/swagger.json new file mode 100644 index 000000000..e20754c9d --- /dev/null +++ b/example/object-map-example/docs/swagger.json @@ -0,0 +1,78 @@ +{ + "swagger": "2.0", + "info": { + "title": "Swagger Map Example API", + "termsOfService": "http://swagger.io/terms/", + "contact": {}, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/api/v1", + "paths": { + "/test": { + "get": { + "description": "get map", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Get Map Example", + "operationId": "get-map", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.Response" + } + } + } + } + } + }, + "definitions": { + "controller.Data": { + "type": "object", + "properties": { + "title": { + "type": "string", + "example": "Object data" + } + } + }, + "controller.Response": { + "type": "object", + "properties": { + "map_data": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "key": "value", + "key2": "value2" + } + }, + "object": { + "$ref": "#/definitions/controller.Data" + }, + "title": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "en": "Map", + "kk": "Карталар", + "ru": "Карта" + } + } + } + } + } +} \ No newline at end of file diff --git a/example/object-map-example/docs/swagger.yaml b/example/object-map-example/docs/swagger.yaml new file mode 100644 index 000000000..d7a65e140 --- /dev/null +++ b/example/object-map-example/docs/swagger.yaml @@ -0,0 +1,53 @@ +basePath: /api/v1 +definitions: + controller.Data: + properties: + title: + example: Object data + type: string + type: object + controller.Response: + properties: + map_data: + additionalProperties: + type: string + example: + key: value + key2: value2 + type: object + object: + $ref: '#/definitions/controller.Data' + title: + additionalProperties: + type: string + example: + en: Map + kk: Карталар + ru: Карта + type: object + type: object +host: localhost:8080 +info: + contact: {} + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + termsOfService: http://swagger.io/terms/ + title: Swagger Map Example API + version: "1.0" +paths: + /test: + get: + consumes: + - application/json + description: get map + operationId: get-map + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.Response' + summary: Get Map Example +swagger: "2.0" diff --git a/example/object-map-example/go.mod b/example/object-map-example/go.mod new file mode 100644 index 000000000..c4b5c151f --- /dev/null +++ b/example/object-map-example/go.mod @@ -0,0 +1,11 @@ +module github.com/swaggo/swag/example/object-map-example + +go 1.14 + +require ( + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 + github.com/gin-gonic/gin v1.6.3 + github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 + github.com/swaggo/gin-swagger v1.3.0 + github.com/swaggo/swag v1.5.1 +) diff --git a/example/object-map-example/main.go b/example/object-map-example/main.go new file mode 100644 index 000000000..0108f2c9f --- /dev/null +++ b/example/object-map-example/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/swaggo/swag/example/object-map-example/controller" + _ "github.com/swaggo/swag/example/object-map-example/docs" + + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +// @title Swagger Map Example API +// @version 1.0 +// @termsOfService http://swagger.io/terms/ + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host localhost:8080 +// @BasePath /api/v1 +func main() { + r := gin.Default() + + c := controller.NewController() + + v1 := r.Group("/api/v1") + { + test := v1.Group("/map") + { + test.GET("", c.GetMap) + } + } + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + r.Run(":8080") +} diff --git a/gen/gen.go b/gen/gen.go index 0edd211c2..291517485 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -8,7 +8,7 @@ import ( "io" "log" "os" - "path" + "path/filepath" "strings" "text/template" "time" @@ -39,6 +39,9 @@ type Config struct { // SearchDir the swag would be parse SearchDir string + // excludes dirs and files in SearchDir,comma separated + Excludes string + // OutputDir represents the output directory for all the generated files OutputDir string @@ -54,11 +57,20 @@ type Config struct { // ParseDependencies whether swag should be parse outside dependency folder ParseDependency bool + // ParseInternal whether swag should parse internal packages + ParseInternal bool + // MarkdownFilesDir used to find markdownfiles, which can be used for tag descriptions MarkdownFilesDir string // GeneratedTime whether swag should generate the timestamp at the top of docs.go GeneratedTime bool + + // CodeExampleFilesDir used to find code example files, which can be used for x-codeSamples + CodeExampleFilesDir string + + // ParseDepth dependency parse depth + ParseDepth int } // Build builds swagger json file for given searchDir and mainAPIFile. Returns json @@ -68,12 +80,15 @@ func (g *Gen) Build(config *Config) error { } log.Println("Generate swagger docs....") - p := swag.New(swag.SetMarkdownFileDirectory(config.MarkdownFilesDir)) + p := swag.New(swag.SetMarkdownFileDirectory(config.MarkdownFilesDir), + swag.SetExcludedDirsAndFiles(config.Excludes), + swag.SetCodeExamplesDirectory(config.CodeExampleFilesDir)) p.PropNamingStrategy = config.PropNamingStrategy p.ParseVendor = config.ParseVendor p.ParseDependency = config.ParseDependency + p.ParseInternal = config.ParseInternal - if err := p.ParseAPI(config.SearchDir, config.MainAPIFile); err != nil { + if err := p.ParseAPI(config.SearchDir, config.MainAPIFile, config.ParseDepth); err != nil { return err } swagger := p.GetSwagger() @@ -87,10 +102,14 @@ func (g *Gen) Build(config *Config) error { return err } - packageName := path.Base(config.OutputDir) - docFileName := path.Join(config.OutputDir, "docs.go") - jsonFileName := path.Join(config.OutputDir, "swagger.json") - yamlFileName := path.Join(config.OutputDir, "swagger.yaml") + absOutputDir, err := filepath.Abs(config.OutputDir) + if err != nil { + return err + } + packageName := filepath.Base(absOutputDir) + docFileName := filepath.Join(config.OutputDir, "docs.go") + jsonFileName := filepath.Join(config.OutputDir, "swagger.json") + yamlFileName := filepath.Join(config.OutputDir, "swagger.yaml") docs, err := os.Create(docFileName) if err != nil { diff --git a/gen/gen_test.go b/gen/gen_test.go index 075917b58..a61ccdec0 100644 --- a/gen/gen_test.go +++ b/gen/gen_test.go @@ -4,7 +4,6 @@ import ( "errors" "os" "os/exec" - "path" "path/filepath" "testing" @@ -24,9 +23,9 @@ func TestGen_Build(t *testing.T) { assert.NoError(t, New().Build(config)) expectedFiles := []string{ - path.Join(config.OutputDir, "docs.go"), - path.Join(config.OutputDir, "swagger.json"), - path.Join(config.OutputDir, "swagger.yaml"), + filepath.Join(config.OutputDir, "docs.go"), + filepath.Join(config.OutputDir, "swagger.json"), + filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { @@ -48,9 +47,9 @@ func TestGen_BuildSnakecase(t *testing.T) { assert.NoError(t, New().Build(config)) expectedFiles := []string{ - path.Join(config.OutputDir, "docs.go"), - path.Join(config.OutputDir, "swagger.json"), - path.Join(config.OutputDir, "swagger.yaml"), + filepath.Join(config.OutputDir, "docs.go"), + filepath.Join(config.OutputDir, "swagger.json"), + filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { @@ -72,9 +71,9 @@ func TestGen_BuildLowerCamelcase(t *testing.T) { assert.NoError(t, New().Build(config)) expectedFiles := []string{ - path.Join(config.OutputDir, "docs.go"), - path.Join(config.OutputDir, "swagger.json"), - path.Join(config.OutputDir, "swagger.yaml"), + filepath.Join(config.OutputDir, "docs.go"), + filepath.Join(config.OutputDir, "swagger.json"), + filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { @@ -116,8 +115,8 @@ func TestGen_jsonToYAML(t *testing.T) { assert.Error(t, gen.Build(config)) expectedFiles := []string{ - path.Join(config.OutputDir, "docs.go"), - path.Join(config.OutputDir, "swagger.json"), + filepath.Join(config.OutputDir, "docs.go"), + filepath.Join(config.OutputDir, "swagger.json"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { @@ -226,9 +225,9 @@ func TestGen_configWithOutputDir(t *testing.T) { assert.NoError(t, New().Build(config)) expectedFiles := []string{ - path.Join(config.OutputDir, "docs.go"), - path.Join(config.OutputDir, "swagger.json"), - path.Join(config.OutputDir, "swagger.yaml"), + filepath.Join(config.OutputDir, "docs.go"), + filepath.Join(config.OutputDir, "swagger.json"), + filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { @@ -334,9 +333,35 @@ func TestGen_GeneratedDoc(t *testing.T) { assert.NoError(t, cmd.Run()) expectedFiles := []string{ - path.Join(config.OutputDir, "docs.go"), - path.Join(config.OutputDir, "swagger.json"), - path.Join(config.OutputDir, "swagger.yaml"), + filepath.Join(config.OutputDir, "docs.go"), + filepath.Join(config.OutputDir, "swagger.json"), + filepath.Join(config.OutputDir, "swagger.yaml"), + } + for _, expectedFile := range expectedFiles { + if _, err := os.Stat(expectedFile); os.IsNotExist(err) { + t.Fatal(err) + } + os.Remove(expectedFile) + } +} + +func TestGen_cgoImports(t *testing.T) { + searchDir := "../testdata/simple_cgo" + + config := &Config{ + SearchDir: searchDir, + MainAPIFile: "./main.go", + OutputDir: "../testdata/simple_cgo/docs", + PropNamingStrategy: "", + ParseDependency: true, + } + + assert.NoError(t, New().Build(config)) + + expectedFiles := []string{ + filepath.Join(config.OutputDir, "docs.go"), + filepath.Join(config.OutputDir, "swagger.json"), + filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { diff --git a/go.mod b/go.mod index f02834645..72d4a5f29 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,12 @@ require ( github.com/KyleBanks/depth v1.2.1 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 github.com/ghodss/yaml v1.0.0 - github.com/gin-gonic/gin v1.4.0 - github.com/go-openapi/jsonreference v0.19.3 - github.com/go-openapi/spec v0.19.4 - github.com/satori/go.uuid v1.2.0 - github.com/stretchr/testify v1.4.0 - github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 - github.com/swaggo/gin-swagger v1.2.0 - github.com/urfave/cli/v2 v2.1.1 - golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 + github.com/go-openapi/spec v0.20.0 + github.com/stretchr/testify v1.7.0 + github.com/urfave/cli/v2 v2.3.0 + golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect + golang.org/x/text v0.3.5 // indirect + golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064 ) -go 1.13 +go 1.15 diff --git a/go.sum b/go.sum index ab7e7c11c..8e5559667 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ 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.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= -github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 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= @@ -11,134 +9,101 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc= -github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= -github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= -github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= -github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= -github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0= -github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.19.0 h1:BqWKpV1dFd+AuiKlgtddwVIFQsuMpxfBDBHGfM2yNpk= -github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4= -github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo= -github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880= -github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +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.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/spec v0.20.0 h1:HGLc8AJ7ynOxwv0Lq4TsnwLsWMawHAYiJIFzbcML86I= +github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/go-openapi/swag v0.19.12 h1:Bc0bnY2c3AoF7Gc+IMIAQQsD8fLHjHpc19wXvYuayQI= +github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= +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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= -github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +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 h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -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.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= -github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= -github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0= -github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= -github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.5-pre h1:jyJKFOSEbdOc2HODrf2qcCkYOdq7zzXqA9bhW5oV4fM= -github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= -github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.5-pre h1:5YV9PsFAN+ndcCtTM7s60no7nY7eTG3LPtxhSwuxzCs= -github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= -github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= -github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190611141213-3f473d35a33a h1:+KkCgOMgnKSgenxTBoiwkMqTiouMIy/3o8RLdmSbGoY= -golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-20190222072716-a9d3bda3a223/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-20190610200419-93c9922d18ae h1:xiXzMMEQdQcric9hXtr1QU98MHunKK7OTtsoU6bYWs4= -golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b h1:/mJ+GKieZA6hFDQGdWZrjj4AXPl5ylY+5HusG80roy0= -golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 h1:QjA/9ArTfVTLfEhClDCG7SGrZkZixxWpwNCDiwJfh88= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064 h1:BmCFkEH4nJrYcAc2L08yX5RhYGD4j58PTMkEUDkpz2I= +golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= -gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +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= diff --git a/operation.go b/operation.go index b3ee04379..6e357aa17 100644 --- a/operation.go +++ b/operation.go @@ -6,13 +6,14 @@ import ( "go/ast" goparser "go/parser" "go/token" + "io/ioutil" "net/http" "os" + "path/filepath" "regexp" "strconv" "strings" - "github.com/go-openapi/jsonreference" "github.com/go-openapi/spec" "golang.org/x/tools/go/loader" ) @@ -24,7 +25,8 @@ type Operation struct { Path string spec.Operation - parser *Parser + parser *Parser + codeExampleFilesDir string } var mimeTypeAliases = map[string]string{ @@ -46,13 +48,34 @@ var mimeTypePattern = regexp.MustCompile("^[^/]+/[^/]+$") // NewOperation creates a new Operation with default properties. // map[int]Response -func NewOperation() *Operation { - return &Operation{ +func NewOperation(parser *Parser, options ...func(*Operation)) *Operation { + if parser == nil { + parser = New() + } + + result := &Operation{ + parser: parser, HTTPMethod: "get", Operation: spec.Operation{ OperationProps: spec.OperationProps{}, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{}, + }, }, } + + for _, option := range options { + option(result) + } + + return result +} + +// SetCodeExampleFilesDirectory sets the directory to search for codeExamples +func SetCodeExampleFilesDirectory(directoryPath string) func(*Operation) { + return func(o *Operation) { + o.codeExampleFilesDir = directoryPath + } } // ParseComment parses comment for given comment string and returns error if error occurs. @@ -69,6 +92,12 @@ func (operation *Operation) ParseComment(comment string, astFile *ast.File) erro switch lowerAttribute { case "@description": operation.ParseDescriptionComment(lineRemainder) + case "@description.markdown": + commentInfo, err := getMarkdownForTag(lineRemainder, operation.parser.markdownFileDir) + if err != nil { + return err + } + operation.ParseDescriptionComment(string(commentInfo)) case "@summary": operation.Summary = lineRemainder case "@id": @@ -81,7 +110,7 @@ func (operation *Operation) ParseComment(comment string, astFile *ast.File) erro err = operation.ParseProduceComment(lineRemainder) case "@param": err = operation.ParseParamComment(lineRemainder, astFile) - case "@success", "@failure": + case "@success", "@failure", "@response": err = operation.ParseResponseComment(lineRemainder, astFile) case "@header": err = operation.ParseResponseHeaderComment(lineRemainder, astFile) @@ -91,13 +120,36 @@ func (operation *Operation) ParseComment(comment string, astFile *ast.File) erro err = operation.ParseSecurityComment(lineRemainder) case "@deprecated": operation.Deprecate() + case "@x-codesamples": + err = operation.ParseCodeSample(attribute, commentLine, lineRemainder) default: err = operation.ParseMetadata(attribute, lowerAttribute, lineRemainder) } - return err } +// ParseCodeSample godoc +func (operation *Operation) ParseCodeSample(attribute, commentLine, lineRemainder string) error { + if lineRemainder == "file" { + data, err := getCodeExampleForSummary(operation.Summary, operation.codeExampleFilesDir) + if err != nil { + return err + } + + var valueJSON interface{} + if err := json.Unmarshal(data, &valueJSON); err != nil { + return fmt.Errorf("annotation %s need a valid json value", attribute) + } + + operation.Extensions[attribute[1:]] = valueJSON // don't use the method provided by spec lib, cause it will call toLower() on attribute names, which is wrongy + + return nil + } + + // Fallback into existing logic + return operation.ParseMetadata(attribute, strings.ToLower(attribute), lineRemainder) +} + // ParseDescriptionComment godoc func (operation *Operation) ParseDescriptionComment(lineRemainder string) { if operation.Description == "" { @@ -119,7 +171,8 @@ func (operation *Operation) ParseMetadata(attribute, lowerAttribute, lineRemaind if err := json.Unmarshal([]byte(lineRemainder), &valueJSON); err != nil { return fmt.Errorf("annotation %s need a valid json value", attribute) } - operation.Operation.AddExtension(attribute[1:], valueJSON) // Trim "@" at head + + operation.Extensions[attribute[1:]] = valueJSON // don't use the method provided by spec lib, cause it will call toLower() on attribute names, which is wrongy } return nil } @@ -140,14 +193,14 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F refType := TransToValidSchemeType(matches[3]) // Detect refType - objectType := "object" - if strings.HasPrefix(refType, "[]") == true { - objectType = "array" + 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" + objectType = PRIMITIVE } requiredText := strings.ToLower(matches[4]) @@ -157,34 +210,28 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F param := createParameter(paramType, description, name, refType, required) switch paramType { - case "path", "header", "formData": + case "path", "header": switch objectType { - case "array", "object": + case ARRAY, OBJECT: return fmt.Errorf("%s is not supported type for %s", refType, paramType) } - case "query": + case "query", "formData": switch objectType { - case "array": + case ARRAY: if !IsPrimitiveType(refType) { return fmt.Errorf("%s is not supported array type for %s", refType, paramType) } - param.SimpleSchema.Type = "array" + param.SimpleSchema.Type = objectType + if operation.parser != nil { + param.CollectionFormat = TransToValidCollectionFormat(operation.parser.collectionFormatInQuery) + } param.SimpleSchema.Items = &spec.Items{ SimpleSchema: spec.SimpleSchema{ Type: refType, }, } - case "object": - refType, typeSpec, err := operation.registerSchemaType(refType, astFile) - if err != nil { - return err - } - structType, ok := typeSpec.Type.(*ast.StructType) - if !ok { - return fmt.Errorf("%s is not supported type for %s", refType, paramType) - } - refSplit := strings.Split(refType, ".") - schema, err := operation.parser.parseStruct(refSplit[0], structType.Fields) + case OBJECT: + schema, err := operation.parser.getTypeSchema(refType, astFile, false) if err != nil { return err } @@ -199,16 +246,22 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F } return false } - for name, prop := range schema.Properties { + items := schema.Properties.ToOrderedSchemaItems() + for _, item := range items { + name := item.Name + prop := item.Schema if len(prop.Type) == 0 { continue } - if prop.Type[0] == "array" && + if prop.Type[0] == ARRAY && prop.Items.Schema != nil && len(prop.Items.Schema.Type) > 0 && IsSimplePrimitiveType(prop.Items.Schema.Type[0]) { param = createParameter(paramType, prop.Description, name, prop.Type[0], find(schema.Required, name)) param.SimpleSchema.Type = prop.Type[0] + if operation.parser != nil && operation.parser.collectionFormatInQuery != "" && param.CollectionFormat == "" { + param.CollectionFormat = TransToValidCollectionFormat(operation.parser.collectionFormatInQuery) + } param.SimpleSchema.Items = &spec.Items{ SimpleSchema: spec.SimpleSchema{ Type: prop.Items.Schema.Type[0], @@ -220,6 +273,11 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F Println(fmt.Sprintf("skip field [%s] in %s is not supported type for %s", name, refType, paramType)) continue } + param.Nullable = prop.Nullable + param.Format = prop.Format + param.Default = prop.Default + param.Example = prop.Example + param.Extensions = prop.Extensions param.CommonValidations.Maximum = prop.Maximum param.CommonValidations.Minimum = prop.Minimum param.CommonValidations.ExclusiveMaximum = prop.ExclusiveMaximum @@ -237,113 +295,42 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F return nil } case "body": - switch objectType { - case "primitive": - param.Schema.Type = spec.StringOrArray{refType} - case "array": - param.Schema.Type = spec.StringOrArray{objectType} - param.Schema.Items = &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{}, - }, - } - // Array of Primitive or Object - if IsPrimitiveType(refType) { - param.Schema.Items.Schema.Type = spec.StringOrArray{refType} - } else { - var err error - refType, _, err = operation.registerSchemaType(refType, astFile) - if err != nil { - return err - } - param.Schema.Items.Schema.Ref = spec.Ref{Ref: jsonreference.MustCreateRef("#/definitions/" + refType)} - } - case "object": - var err error - refType, _, err = operation.registerSchemaType(refType, astFile) - if err != nil { - return err - } - param.Schema.Type = []string{} - param.Schema.Ref = spec.Ref{ - Ref: jsonreference.MustCreateRef("#/definitions/" + refType), - } + schema, err := operation.parseAPIObjectSchema(objectType, refType, astFile) + if err != nil { + return err } + param.Schema = schema default: return fmt.Errorf("%s is not supported paramType", paramType) } - if err := operation.parseAndExtractionParamAttribute(commentLine, refType, ¶m); err != nil { + if err := operation.parseAndExtractionParamAttribute(commentLine, objectType, refType, ¶m); err != nil { return err } operation.Operation.Parameters = append(operation.Operation.Parameters, param) return nil } -func (operation *Operation) registerSchemaType(schemaType string, astFile *ast.File) (string, *ast.TypeSpec, error) { - if !strings.ContainsRune(schemaType, '.') { - if astFile == nil { - return schemaType, nil, fmt.Errorf("no package name for type %s", schemaType) - } - schemaType = astFile.Name.String() + "." + schemaType - } - refSplit := strings.Split(schemaType, ".") - pkgName := refSplit[0] - typeName := refSplit[1] - if typeSpec, ok := operation.parser.TypeDefinitions[pkgName][typeName]; ok { - operation.parser.registerTypes[schemaType] = typeSpec - return schemaType, typeSpec, nil - } - var typeSpec *ast.TypeSpec - if astFile == nil { - return schemaType, nil, fmt.Errorf("can not register schema type: %q reason: astFile == nil", schemaType) - } - for _, imp := range astFile.Imports { - if imp.Name != nil && imp.Name.Name == pkgName { // the import had an alias that matched - break - } - impPath := strings.Replace(imp.Path.Value, `"`, ``, -1) - if strings.HasSuffix(impPath, "/"+pkgName) { - var err error - typeSpec, err = findTypeDef(impPath, typeName) - if err != nil { - return schemaType, nil, fmt.Errorf("can not find type def: %q error: %s", schemaType, err) - } - break - } - } - - if typeSpec == nil { - return schemaType, nil, fmt.Errorf("can not find schema type: %q", schemaType) - } - - if _, ok := operation.parser.TypeDefinitions[pkgName]; !ok { - operation.parser.TypeDefinitions[pkgName] = make(map[string]*ast.TypeSpec) - } - - operation.parser.TypeDefinitions[pkgName][typeName] = typeSpec - operation.parser.registerTypes[schemaType] = typeSpec - return schemaType, typeSpec, nil -} - var regexAttributes = map[string]*regexp.Regexp{ // for Enums(A, B) - "enums": regexp.MustCompile(`(?i)enums\(.*\)`), - // for Minimum(0) - "maxinum": regexp.MustCompile(`(?i)maxinum\(.*\)`), - // for Maximum(0) - "mininum": regexp.MustCompile(`(?i)mininum\(.*\)`), - // for Maximum(0) - "default": regexp.MustCompile(`(?i)default\(.*\)`), + "enums": regexp.MustCompile(`(?i)\s+enums\(.*\)`), + // for maximum(0) + "maximum": regexp.MustCompile(`(?i)\s+maxinum|maximum\(.*\)`), + // for minimum(0) + "minimum": regexp.MustCompile(`(?i)\s+mininum|minimum\(.*\)`), + // for default(0) + "default": regexp.MustCompile(`(?i)\s+default\(.*\)`), // for minlength(0) - "minlength": regexp.MustCompile(`(?i)minlength\(.*\)`), + "minlength": regexp.MustCompile(`(?i)\s+minlength\(.*\)`), // for maxlength(0) - "maxlength": regexp.MustCompile(`(?i)maxlength\(.*\)`), + "maxlength": regexp.MustCompile(`(?i)\s+maxlength\(.*\)`), // for format(email) - "format": regexp.MustCompile(`(?i)format\(.*\)`), + "format": regexp.MustCompile(`(?i)\s+format\(.*\)`), + // for collectionFormat(csv) + "collectionFormat": regexp.MustCompile(`(?i)\s+collectionFormat\(.*\)`), } -func (operation *Operation) parseAndExtractionParamAttribute(commentLine, schemaType string, param *spec.Parameter) error { +func (operation *Operation) parseAndExtractionParamAttribute(commentLine, objectType, schemaType string, param *spec.Parameter) error { schemaType = TransToValidSchemeType(schemaType) for attrKey, re := range regexAttributes { attr, err := findAttr(re, commentLine) @@ -356,13 +343,13 @@ func (operation *Operation) parseAndExtractionParamAttribute(commentLine, schema if err != nil { return err } - case "maxinum": + case "maximum": n, err := setNumberParam(attrKey, schemaType, attr, commentLine) if err != nil { return err } param.Maximum = &n - case "mininum": + case "minimum": n, err := setNumberParam(attrKey, schemaType, attr, commentLine) if err != nil { return err @@ -388,8 +375,13 @@ func (operation *Operation) parseAndExtractionParamAttribute(commentLine, schema param.MinLength = &n case "format": param.Format = attr + case "collectionFormat": + n, err := setCollectionFormatParam(attrKey, objectType, attr, commentLine) + if err != nil { + return err + } + param.CollectionFormat = n } - } return nil } @@ -405,7 +397,7 @@ func findAttr(re *regexp.Regexp, commentLine string) (string, error) { } func setStringParam(name, schemaType, attr, commentLine string) (int64, error) { - if schemaType != "string" { + if schemaType != STRING { return 0, fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) } n, err := strconv.ParseInt(attr, 10, 64) @@ -416,7 +408,7 @@ func setStringParam(name, schemaType, attr, commentLine string) (int64, error) { } func setNumberParam(name, schemaType, attr, commentLine string) (float64, error) { - if schemaType != "integer" && schemaType != "number" { + if schemaType != INTEGER && schemaType != NUMBER { return 0, fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) } n, err := strconv.ParseFloat(attr, 64) @@ -439,25 +431,32 @@ func setEnumParam(attr, schemaType string, param *spec.Parameter) error { return nil } +func setCollectionFormatParam(name, schemaType, attr, commentLine string) (string, error) { + if schemaType != ARRAY { + return "", fmt.Errorf("%s is attribute to set to an array. comment=%s got=%s", name, commentLine, schemaType) + } + return TransToValidCollectionFormat(attr), nil +} + // defineType enum value define the type (object and array unsupported) func defineType(schemaType string, value string) (interface{}, error) { schemaType = TransToValidSchemeType(schemaType) switch schemaType { - case "string": + case STRING: return value, nil - case "number": + case NUMBER: v, err := strconv.ParseFloat(value, 64) if err != nil { return nil, fmt.Errorf("enum value %s can't convert to %s err: %s", value, schemaType, err) } return v, nil - case "integer": + case INTEGER: v, err := strconv.Atoi(value) if err != nil { return nil, fmt.Errorf("enum value %s can't convert to %s err: %s", value, schemaType, err) } return v, nil - case "boolean": + case BOOLEAN: v, err := strconv.ParseBool(value) if err != nil { return nil, fmt.Errorf("enum value %s can't convert to %s err: %s", value, schemaType, err) @@ -604,82 +603,162 @@ func findTypeDef(importPath, typeName string) (*ast.TypeSpec, error) { return nil, fmt.Errorf("type spec not found") } -var responsePattern = regexp.MustCompile(`([\d]+)[\s]+([\w\{\}]+)[\s]+([\w\-\.\/]+)[^"]*(.*)?`) +var responsePattern = regexp.MustCompile(`^([\w,]+)[\s]+([\w\{\}]+)[\s]+([\w\-\.\/\{\}=,\[\]]+)[^"]*(.*)?`) -// ParseResponseComment parses comment for given `response` comment string. -func (operation *Operation) ParseResponseComment(commentLine string, astFile *ast.File) error { - var matches []string +//ResponseType{data1=Type1,data2=Type2} +var combinedPattern = regexp.MustCompile(`^([\w\-\.\/\[\]]+)\{(.*)\}$`) - if matches = responsePattern.FindStringSubmatch(commentLine); len(matches) != 5 { - err := operation.ParseEmptyResponseComment(commentLine) +func (operation *Operation) parseObjectSchema(refType string, astFile *ast.File) (*spec.Schema, error) { + switch { + case refType == "interface{}": + return PrimitiveSchema(OBJECT), nil + case IsGolangPrimitiveType(refType): + refType = TransToValidSchemeType(refType) + return PrimitiveSchema(refType), nil + case IsPrimitiveType(refType): + return PrimitiveSchema(refType), nil + case strings.HasPrefix(refType, "[]"): + schema, err := operation.parseObjectSchema(refType[2:], astFile) if err != nil { - return operation.ParseEmptyResponseOnly(commentLine) + return nil, err } - return err - } + return spec.ArrayProperty(schema), 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{}" { + return spec.MapProperty(nil), nil - response := spec.Response{} + } + schema, err := operation.parseObjectSchema(refType, astFile) + if err != nil { + return nil, err + } + return spec.MapProperty(schema), nil + case strings.Contains(refType, "{"): + return operation.parseCombinedObjectSchema(refType, astFile) + default: + if operation.parser != nil { // checking refType has existing in 'TypeDefinitions' + schema, err := operation.parser.getTypeSchema(refType, astFile, true) + if err != nil { + return nil, err + } + return schema, nil + } - code, _ := strconv.Atoi(matches[1]) + return RefSchema(refType), nil + } +} - responseDescription := strings.Trim(matches[4], "\"") - if responseDescription == "" { - responseDescription = http.StatusText(code) +func (operation *Operation) parseCombinedObjectSchema(refType string, astFile *ast.File) (*spec.Schema, error) { + matches := combinedPattern.FindStringSubmatch(refType) + if len(matches) != 3 { + return nil, fmt.Errorf("invalid type: %s", refType) + } + refType = matches[1] + schema, err := operation.parseObjectSchema(refType, astFile) + if err != nil { + return nil, err } - response.Description = responseDescription - schemaType := strings.Trim(matches[2], "{}") - refType := matches[3] + parseFields := func(s string) []string { + n := 0 + return strings.FieldsFunc(s, func(r rune) bool { + if r == '{' { + n++ + return false + } else if r == '}' { + n-- + return false + } + return r == ',' && n == 0 + }) + } - if !IsGolangPrimitiveType(refType) { - if operation.parser != nil { // checking refType has existing in 'TypeDefinitions' - var err error - if refType, _, err = operation.registerSchemaType(refType, astFile); err != nil { - return err + fields := parseFields(matches[2]) + props := map[string]spec.Schema{} + for _, field := range fields { + if matches := strings.SplitN(field, "=", 2); len(matches) == 2 { + schema, err := operation.parseObjectSchema(matches[1], astFile) + if err != nil { + return nil, err } + props[matches[0]] = *schema } } - // so we have to know all type in app - response.Schema = &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{schemaType}}} + if len(props) == 0 { + return schema, nil + } + return spec.ComposedSchema(*schema, spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{OBJECT}, + Properties: props, + }, + }), nil +} - if schemaType == "object" { - response.Schema.SchemaProps = spec.SchemaProps{} - response.Schema.Ref = spec.Ref{ - Ref: jsonreference.MustCreateRef("#/definitions/" + refType), +func (operation *Operation) parseAPIObjectSchema(schemaType, refType string, astFile *ast.File) (*spec.Schema, error) { + switch schemaType { + case OBJECT: + if !strings.HasPrefix(refType, "[]") { + return operation.parseObjectSchema(refType, astFile) } + refType = refType[2:] + fallthrough + case ARRAY: + schema, err := operation.parseObjectSchema(refType, astFile) + if err != nil { + return nil, err + } + return spec.ArrayProperty(schema), nil + case PRIMITIVE: + return PrimitiveSchema(refType), nil + default: + return PrimitiveSchema(schemaType), nil } +} - if schemaType == "array" { - refType = TransToValidSchemeType(refType) - if IsPrimitiveType(refType) { - response.Schema.Items = &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: spec.StringOrArray{refType}, - }, - }, - } - } else { - response.Schema.Items = &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Ref: spec.Ref{Ref: jsonreference.MustCreateRef("#/definitions/" + refType)}, - }, - }, - } +// ParseResponseComment parses comment for given `response` comment string. +func (operation *Operation) ParseResponseComment(commentLine string, astFile *ast.File) error { + var matches []string + + if matches = responsePattern.FindStringSubmatch(commentLine); len(matches) != 5 { + err := operation.ParseEmptyResponseComment(commentLine) + if err != nil { + return operation.ParseEmptyResponseOnly(commentLine) } + return err } - if operation.Responses == nil { - operation.Responses = &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: make(map[int]spec.Response), - }, - } + responseDescription := strings.Trim(matches[4], "\"") + schemaType := strings.Trim(matches[2], "{}") + refType := matches[3] + schema, err := operation.parseAPIObjectSchema(schemaType, refType, astFile) + if err != nil { + return err } - operation.Responses.StatusCodeResponses[code] = response + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, "default") { + operation.DefaultResponse().Schema = schema + operation.DefaultResponse().Description = responseDescription + } else if code, err := strconv.Atoi(codeStr); err == nil { + resp := &spec.Response{ + ResponseProps: spec.ResponseProps{Schema: schema, Description: responseDescription}, + } + if resp.Description == "" { + resp.Description = http.StatusText(code) + } + operation.AddResponse(code, resp) + } else { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + } return nil } @@ -692,45 +771,60 @@ func (operation *Operation) ParseResponseHeaderComment(commentLine string, astFi return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } - response := spec.Response{} - - code, _ := strconv.Atoi(matches[1]) - - responseDescription := strings.Trim(matches[4], "\"") - if responseDescription == "" { - responseDescription = http.StatusText(code) - } - response.Description = responseDescription - schemaType := strings.Trim(matches[2], "{}") - refType := matches[3] - - if operation.Responses == nil { - operation.Responses = &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: make(map[int]spec.Response), - }, + headerKey := matches[3] + description := strings.Trim(matches[4], "\"") + header := spec.Header{} + header.Description = description + header.Type = schemaType + + if strings.EqualFold(matches[1], "all") { + if operation.Responses.Default != nil { + if operation.Responses.Default.Headers == nil { + operation.Responses.Default.Headers = make(map[string]spec.Header) + } + operation.Responses.Default.Headers[headerKey] = header } + if operation.Responses != nil && operation.Responses.StatusCodeResponses != nil { + for code, response := range operation.Responses.StatusCodeResponses { + if response.Headers == nil { + response.Headers = make(map[string]spec.Header) + } + response.Headers[headerKey] = header + operation.Responses.StatusCodeResponses[code] = response + } + } + return nil } - response, responseExist := operation.Responses.StatusCodeResponses[code] - if responseExist { - header := spec.Header{} - header.Description = responseDescription - header.Type = schemaType + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, "default") { + if operation.Responses.Default != nil { + if operation.Responses.Default.Headers == nil { + operation.Responses.Default.Headers = make(map[string]spec.Header) + } + operation.Responses.Default.Headers[headerKey] = header + } + } else if code, err := strconv.Atoi(codeStr); err == nil { + if operation.Responses != nil && operation.Responses.StatusCodeResponses != nil { + if response, responseExist := operation.Responses.StatusCodeResponses[code]; responseExist { + if response.Headers == nil { + response.Headers = make(map[string]spec.Header) + } + response.Headers[headerKey] = header - if response.Headers == nil { - response.Headers = make(map[string]spec.Header) + operation.Responses.StatusCodeResponses[code] = response + } + } + } else { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } - response.Headers[refType] = header - - operation.Responses.StatusCodeResponses[code] = response } return nil } -var emptyResponsePattern = regexp.MustCompile(`([\d]+)[\s]+"(.*)"`) +var emptyResponsePattern = regexp.MustCompile(`([\w,]+)[\s]+"(.*)"`) // ParseEmptyResponseComment parse only comment out status code and description,eg: @Success 200 "it's ok" func (operation *Operation) ParseEmptyResponseComment(commentLine string) error { @@ -740,33 +834,49 @@ func (operation *Operation) ParseEmptyResponseComment(commentLine string) error return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } - response := spec.Response{} - - code, _ := strconv.Atoi(matches[1]) - - response.Description = strings.Trim(matches[2], "") - - if operation.Responses == nil { - operation.Responses = &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: make(map[int]spec.Response), - }, + responseDescription := strings.Trim(matches[2], "\"") + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, "default") { + operation.DefaultResponse().Description = responseDescription + } else if code, err := strconv.Atoi(codeStr); err == nil { + var response spec.Response + response.Description = responseDescription + operation.AddResponse(code, &response) + } else { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } } - operation.Responses.StatusCodeResponses[code] = response - return nil } //ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200 func (operation *Operation) ParseEmptyResponseOnly(commentLine string) error { - response := spec.Response{} + for _, codeStr := range strings.Split(commentLine, ",") { + if strings.EqualFold(codeStr, "default") { + _ = operation.DefaultResponse() + } else if code, err := strconv.Atoi(codeStr); err == nil { + var response spec.Response + //response.Description = http.StatusText(code) + operation.AddResponse(code, &response) + } else { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + } - code, err := strconv.Atoi(commentLine) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + return nil +} + +//DefaultResponse return the default response member pointer +func (operation *Operation) DefaultResponse() *spec.Response { + if operation.Responses.Default == nil { + operation.Responses.Default = &spec.Response{} } + return operation.Responses.Default +} + +//AddResponse add a response for a code +func (operation *Operation) AddResponse(code int, response *spec.Response) { if operation.Responses == nil { operation.Responses = &spec.Responses{ ResponsesProps: spec.ResponsesProps{ @@ -774,10 +884,7 @@ func (operation *Operation) ParseEmptyResponseOnly(commentLine string) error { }, } } - - operation.Responses.StatusCodeResponses[code] = response - - return nil + operation.Responses.StatusCodeResponses[code] = *response } // createParameter returns swagger spec.Parameter for gived paramType, description, paramName, schemaType, required @@ -808,3 +915,31 @@ func createParameter(paramType, description, paramName, schemaType string, requi } return parameter } + +func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error) { + filesInfos, err := ioutil.ReadDir(dirPath) + if err != nil { + return nil, err + } + + for _, fileInfo := range filesInfos { + if fileInfo.IsDir() { + continue + } + fileName := fileInfo.Name() + + if !strings.Contains(fileName, ".json") { + continue + } + + if strings.Contains(fileName, summaryName) { + fullPath := filepath.Join(dirPath, fileName) + commentInfo, err := ioutil.ReadFile(fullPath) + if err != nil { + return nil, fmt.Errorf("Failed to read code example file %s error: %s ", fullPath, err) + } + return commentInfo, nil + } + } + return nil, 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 ee58db0f6..1e982d593 100644 --- a/operation_test.go +++ b/operation_test.go @@ -2,7 +2,6 @@ package swag import ( "encoding/json" - "go/ast" goparser "go/parser" "go/token" "testing" @@ -12,7 +11,7 @@ import ( ) func TestParseEmptyComment(t *testing.T) { - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment("//", nil) assert.NoError(t, err) @@ -27,7 +26,7 @@ func TestParseTagsComment(t *testing.T) { ] }` comment := `/@Tags pet, store,user` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) b, _ := json.MarshalIndent(operation, "", " ") @@ -54,7 +53,7 @@ func TestParseAcceptComment(t *testing.T) { ] }` comment := `/@Accept json,xml,plain,html,mpfd,x-www-form-urlencoded,json-api,json-stream,octet-stream,png,jpeg,gif,application/xhtml+xml,application/health+json` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) b, _ := json.MarshalIndent(operation, "", " ") @@ -64,7 +63,7 @@ func TestParseAcceptComment(t *testing.T) { func TestParseAcceptCommentErr(t *testing.T) { comment := `/@Accept unknown` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) } @@ -104,7 +103,7 @@ func TestParseProduceCommentErr(t *testing.T) { func TestParseRouterComment(t *testing.T) { comment := `/@Router /customer/get-wishlist/{wishlist_id} [get]` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) assert.Equal(t, "/customer/get-wishlist/{wishlist_id}", operation.Path) @@ -113,7 +112,7 @@ func TestParseRouterComment(t *testing.T) { func TestParseRouterOnlySlash(t *testing.T) { comment := `// @Router / [get]` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) assert.Equal(t, "/", operation.Path) @@ -122,7 +121,7 @@ func TestParseRouterOnlySlash(t *testing.T) { func TestParseRouterCommentWithPlusSign(t *testing.T) { comment := `/@Router /customer/get-wishlist/{proxy+} [post]` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) assert.Equal(t, "/customer/get-wishlist/{proxy+}", operation.Path) @@ -131,7 +130,7 @@ func TestParseRouterCommentWithPlusSign(t *testing.T) { func TestParseRouterCommentWithColonSign(t *testing.T) { comment := `/@Router /customer/get-wishlist/{wishlist_id}:move [post]` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) assert.Equal(t, "/customer/get-wishlist/{wishlist_id}:move", operation.Path) @@ -140,32 +139,29 @@ func TestParseRouterCommentWithColonSign(t *testing.T) { func TestParseRouterCommentNoColonSignAtPathStartErr(t *testing.T) { comment := `/@Router :customer/get-wishlist/{wishlist_id}:move [post]` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) } func TestParseRouterCommentMethodSeparationErr(t *testing.T) { comment := `/@Router /api/{id}|,*[get` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) } func TestParseRouterCommentMethodMissingErr(t *testing.T) { comment := `/@Router /customer/get-wishlist/{wishlist_id}` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) } func TestParseResponseCommentWithObjectType(t *testing.T) { comment := `@Success 200 {object} model.OrderRow "Error message, if code != 200` - operation := NewOperation() - operation.parser = New() - - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["OrderRow"] = &ast.TypeSpec{} + operation := NewOperation(nil) + operation.parser.addTestType("model.OrderRow") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -188,18 +184,405 @@ func TestParseResponseCommentWithObjectType(t *testing.T) { assert.Equal(t, expected, string(b)) } +func TestParseResponseCommentWithNestedPrimitiveType(t *testing.T) { + comment := `@Success 200 {object} model.CommonHeader{data=string,data2=int} "Error message, if code != 200` + operation := NewOperation(nil) + + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + response := operation.Responses.StatusCodeResponses[200] + assert.Equal(t, `Error message, if code != 200`, response.Description) + + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "Error message, if code != 200", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/model.CommonHeader" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "data2": { + "type": "integer" + } + } + } + ] + } + } + } +}` + assert.Equal(t, expected, string(b)) +} + +func TestParseResponseCommentWithNestedPrimitiveArrayType(t *testing.T) { + comment := `@Success 200 {object} model.CommonHeader{data=[]string,data2=[]int} "Error message, if code != 200` + operation := NewOperation(nil) + + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + response := operation.Responses.StatusCodeResponses[200] + assert.Equal(t, `Error message, if code != 200`, response.Description) + + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "Error message, if code != 200", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/model.CommonHeader" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + }, + "data2": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + ] + } + } + } +}` + assert.Equal(t, expected, string(b)) +} + +func TestParseResponseCommentWithNestedObjectType(t *testing.T) { + comment := `@Success 200 {object} model.CommonHeader{data=model.Payload,data2=model.Payload2} "Error message, if code != 200` + operation := NewOperation(nil) + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.Payload2") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + response := operation.Responses.StatusCodeResponses[200] + assert.Equal(t, `Error message, if code != 200`, response.Description) + + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "Error message, if code != 200", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/model.CommonHeader" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Payload" + }, + "data2": { + "$ref": "#/definitions/model.Payload2" + } + } + } + ] + } + } + } +}` + assert.Equal(t, expected, string(b)) +} + +func TestParseResponseCommentWithNestedArrayObjectType(t *testing.T) { + comment := `@Success 200 {object} model.CommonHeader{data=[]model.Payload,data2=[]model.Payload2} "Error message, if code != 200` + operation := NewOperation(nil) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.Payload2") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + response := operation.Responses.StatusCodeResponses[200] + assert.Equal(t, `Error message, if code != 200`, response.Description) + + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "Error message, if code != 200", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/model.CommonHeader" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Payload" + } + }, + "data2": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Payload2" + } + } + } + } + ] + } + } + } +}` + assert.Equal(t, expected, string(b)) +} + +func TestParseResponseCommentWithNestedFields(t *testing.T) { + comment := `@Success 200 {object} model.CommonHeader{data1=int,data2=[]int,data3=model.Payload,data4=[]model.Payload} "Error message, if code != 200` + operation := NewOperation(nil) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + response := operation.Responses.StatusCodeResponses[200] + assert.Equal(t, `Error message, if code != 200`, response.Description) + + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "Error message, if code != 200", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/model.CommonHeader" + }, + { + "type": "object", + "properties": { + "data1": { + "type": "integer" + }, + "data2": { + "type": "array", + "items": { + "type": "integer" + } + }, + "data3": { + "$ref": "#/definitions/model.Payload" + }, + "data4": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Payload" + } + } + } + } + ] + } + } + } +}` + assert.Equal(t, expected, string(b)) +} + +func TestParseResponseCommentWithDeepNestedFields(t *testing.T) { + 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 := NewOperation(nil) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.DeepPayload") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + response := operation.Responses.StatusCodeResponses[200] + assert.Equal(t, `Error message, if code != 200`, response.Description) + + b, _ := json.MarshalIndent(operation, "", " ") + expected := `{ + "responses": { + "200": { + "description": "Error message, if code != 200", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/model.CommonHeader" + }, + { + "type": "object", + "properties": { + "data1": { + "type": "integer" + }, + "data2": { + "type": "array", + "items": { + "type": "integer" + } + }, + "data3": { + "allOf": [ + { + "$ref": "#/definitions/model.Payload" + }, + { + "type": "object", + "properties": { + "data1": { + "type": "integer" + }, + "data2": { + "$ref": "#/definitions/model.DeepPayload" + } + } + } + ] + }, + "data4": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/model.Payload" + }, + { + "type": "object", + "properties": { + "data1": { + "type": "array", + "items": { + "type": "integer" + } + }, + "data2": { + "type": "array", + "items": { + "$ref": "#/definitions/model.DeepPayload" + } + } + } + } + ] + } + } + } + } + ] + } + } + } +}` + assert.Equal(t, expected, string(b)) +} + +func TestParseResponseCommentWithNestedArrayMapFields(t *testing.T) { + comment := `@Success 200 {object} []map[string]model.CommonHeader{data1=[]map[string]model.Payload,data2=map[string][]int} "Error message, if code != 200` + operation := NewOperation(nil) + + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + response := operation.Responses.StatusCodeResponses[200] + assert.Equal(t, `Error message, if code != 200`, response.Description) + + b, _ := json.MarshalIndent(operation, "", " ") + expected := `{ + "responses": { + "200": { + "description": "Error message, if code != 200", + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "allOf": [ + { + "$ref": "#/definitions/model.CommonHeader" + }, + { + "type": "object", + "properties": { + "data1": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/model.Payload" + } + } + }, + "data2": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + ] + } + } + } + } + } +}` + assert.Equal(t, expected, string(b)) +} + func TestParseResponseCommentWithObjectTypeInSameFile(t *testing.T) { comment := `@Success 200 {object} testOwner "Error message, if code != 200"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["swag"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["swag"]["testOwner"] = &ast.TypeSpec{} + 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) @@ -231,11 +614,9 @@ func TestParseResponseCommentWithObjectTypeAnonymousField(t *testing.T) { func TestParseResponseCommentWithObjectTypeErr(t *testing.T) { comment := `@Success 200 {object} model.OrderRow "Error message, if code != 200"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["notexist"] = &ast.TypeSpec{} + operation.parser.addTestType("model.notexist") err := operation.ParseComment(comment, nil) assert.Error(t, err) @@ -243,7 +624,8 @@ func TestParseResponseCommentWithObjectTypeErr(t *testing.T) { func TestParseResponseCommentWithArrayType(t *testing.T) { comment := `@Success 200 {array} model.OrderRow "Error message, if code != 200` - operation := NewOperation() + operation := NewOperation(nil) + operation.parser.addTestType("model.OrderRow") err := operation.ParseComment(comment, nil) assert.NoError(t, err) response := operation.Responses.StatusCodeResponses[200] @@ -271,7 +653,7 @@ func TestParseResponseCommentWithArrayType(t *testing.T) { func TestParseResponseCommentWithBasicType(t *testing.T) { comment := `@Success 200 {string} string "it's ok'"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err, "ParseComment should not fail") b, _ := json.MarshalIndent(operation, "", " ") @@ -289,9 +671,59 @@ func TestParseResponseCommentWithBasicType(t *testing.T) { assert.Equal(t, expected, string(b)) } +func TestParseResponseCommentWithBasicTypeAndCodes(t *testing.T) { + comment := `@Success 200,201,default {string} string "it's ok"` + operation := NewOperation(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "it's ok", + "schema": { + "type": "string" + } + }, + "201": { + "description": "it's ok", + "schema": { + "type": "string" + } + }, + "default": { + "description": "it's ok", + "schema": { + "type": "string" + } + } + } +}` + assert.Equal(t, expected, string(b)) +} + func TestParseEmptyResponseComment(t *testing.T) { - comment := `@Success 200 "it's ok"` - operation := NewOperation() + comment := `@Success 200 "it is ok"` + operation := NewOperation(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "it is ok" + } + } +}` + assert.Equal(t, expected, string(b)) +} + +func TestParseEmptyResponseCommentWithCodes(t *testing.T) { + comment := `@Success 200,201,default "it is ok"` + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err, "ParseComment should not fail") @@ -300,7 +732,13 @@ func TestParseEmptyResponseComment(t *testing.T) { expected := `{ "responses": { "200": { - "description": "it's ok" + "description": "it is ok" + }, + "201": { + "description": "it is ok" + }, + "default": { + "description": "it is ok" } } }` @@ -309,7 +747,7 @@ func TestParseEmptyResponseComment(t *testing.T) { func TestParseResponseCommentWithHeader(t *testing.T) { comment := `@Success 200 "it's ok"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err, "ParseComment should not fail") @@ -338,12 +776,97 @@ func TestParseResponseCommentWithHeader(t *testing.T) { comment = `@Header 200 "Mallformed"` err = operation.ParseComment(comment, nil) assert.Error(t, err, "ParseComment should not fail") +} + +func TestParseResponseCommentWithHeaderForCodes(t *testing.T) { + operation := NewOperation(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") + + b, err := json.MarshalIndent(operation, "", " ") + assert.NoError(t, err) + expected := `{ + "responses": { + "200": { + "description": "it's ok", + "headers": { + "Token": { + "type": "string", + "description": "qwerty" + }, + "Token2": { + "type": "string", + "description": "qwerty" + } + } + }, + "201": { + "description": "it's ok", + "headers": { + "Token": { + "type": "string", + "description": "qwerty" + }, + "Token2": { + "type": "string", + "description": "qwerty" + } + } + }, + "default": { + "description": "it's ok", + "headers": { + "Token": { + "type": "string", + "description": "qwerty" + }, + "Token2": { + "type": "string", + "description": "qwerty" + } + } + } + } +}` + assert.Equal(t, expected, string(b)) + + comment = `@Header 200 "Mallformed"` + err = operation.ParseComment(comment, nil) + assert.Error(t, err, "ParseComment should not fail") } func TestParseEmptyResponseOnlyCode(t *testing.T) { comment := `@Success 200` - operation := NewOperation() + operation := NewOperation(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "" + } + } +}` + assert.Equal(t, expected, string(b)) +} + +func TestParseEmptyResponseOnlyCodes(t *testing.T) { + comment := `@Success 200,201,default` + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err, "ParseComment should not fail") @@ -351,24 +874,40 @@ func TestParseEmptyResponseOnlyCode(t *testing.T) { expected := `{ "responses": { - "200": {} + "200": { + "description": "" + }, + "201": { + "description": "" + }, + "default": { + "description": "" + } } }` assert.Equal(t, expected, string(b)) } func TestParseResponseCommentParamMissing(t *testing.T) { - operation := NewOperation() + operation := NewOperation(nil) - paramLenErrComment := `@Success notIntCode {string}` + paramLenErrComment := `@Success notIntCode` paramLenErr := operation.ParseComment(paramLenErrComment, nil) - assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode {string}"`) + 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""`) } // Test ParseParamComment func TestParseParamCommentByPathType(t *testing.T) { comment := `@Param some_id path int true "Some ID"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -390,7 +929,7 @@ func TestParseParamCommentByPathType(t *testing.T) { // Test ParseParamComment Query Params func TestParseParamCommentBodyArray(t *testing.T) { comment := `@Param names body []string true "Users List"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -417,7 +956,32 @@ func TestParseParamCommentBodyArray(t *testing.T) { // Test ParseParamComment Query Params func TestParseParamCommentQueryArray(t *testing.T) { comment := `@Param names query []string true "Users List"` - operation := NewOperation() + operation := NewOperation(nil) + err := operation.ParseComment(comment, nil) + + assert.NoError(t, err) + b, _ := json.MarshalIndent(operation, "", " ") + expected := `{ + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "description": "Users List", + "name": "names", + "in": "query", + "required": true + } + ] +}` + assert.Equal(t, expected, string(b)) +} + +// Test ParseParamComment Query Params +func TestParseParamCommentQueryArrayFormat(t *testing.T) { + comment := `@Param names query []string true "Users List" collectionFormat(multi)` + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -429,6 +993,7 @@ func TestParseParamCommentQueryArray(t *testing.T) { "items": { "type": "string" }, + "collectionFormat": "multi", "description": "Users List", "name": "names", "in": "query", @@ -441,7 +1006,7 @@ func TestParseParamCommentQueryArray(t *testing.T) { func TestParseParamCommentByID(t *testing.T) { comment := `@Param unsafe_id[lte] query int true "Unsafe query param"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -462,7 +1027,7 @@ func TestParseParamCommentByID(t *testing.T) { func TestParseParamCommentByQueryType(t *testing.T) { comment := `@Param some_id query int true "Some ID"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -483,11 +1048,9 @@ func TestParseParamCommentByQueryType(t *testing.T) { func TestParseParamCommentByBodyType(t *testing.T) { comment := `@Param some_id body model.OrderRow true "Some ID"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["OrderRow"] = &ast.TypeSpec{} + operation.parser.addTestType("model.OrderRow") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -508,10 +1071,54 @@ func TestParseParamCommentByBodyType(t *testing.T) { assert.Equal(t, expected, string(b)) } +func TestParseParamCommentByBodyTypeWithDeepNestedFields(t *testing.T) { + comment := `@Param body body model.CommonHeader{data=string,data2=int} true "test deep"` + operation := NewOperation(nil) + + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + assert.Len(t, operation.Parameters, 1) + assert.Equal(t, "test deep", operation.Parameters[0].Description) + assert.True(t, operation.Parameters[0].Required) + + b, err := json.MarshalIndent(operation, "", " ") + assert.NoError(t, err) + expected := `{ + "parameters": [ + { + "description": "test deep", + "name": "body", + "in": "body", + "required": true, + "schema": { + "allOf": [ + { + "$ref": "#/definitions/model.CommonHeader" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "data2": { + "type": "integer" + } + } + } + ] + } + } + ] +}` + assert.Equal(t, expected, string(b)) +} + func TestParseParamCommentByBodyTypeArrayOfPrimitiveGo(t *testing.T) { comment := `@Param some_id body []int true "Some ID"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -535,13 +1142,57 @@ func TestParseParamCommentByBodyTypeArrayOfPrimitiveGo(t *testing.T) { assert.Equal(t, expected, string(b)) } +func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoWithDeepNestedFields(t *testing.T) { + comment := `@Param body body []model.CommonHeader{data=string,data2=int} true "test deep"` + operation := NewOperation(nil) + operation.parser.addTestType("model.CommonHeader") + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + assert.Len(t, operation.Parameters, 1) + assert.Equal(t, "test deep", operation.Parameters[0].Description) + assert.True(t, operation.Parameters[0].Required) + + b, err := json.MarshalIndent(operation, "", " ") + assert.NoError(t, err) + expected := `{ + "parameters": [ + { + "description": "test deep", + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/model.CommonHeader" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "data2": { + "type": "integer" + } + } + } + ] + } + } + } + ] +}` + assert.Equal(t, expected, string(b)) +} + func TestParseParamCommentByBodyTypeErr(t *testing.T) { comment := `@Param some_id body model.OrderRow true "Some ID"` - operation := NewOperation() - operation.parser = New() - - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["notexist"] = &ast.TypeSpec{} + operation := NewOperation(nil) + operation.parser.addTestType("model.notexist") err := operation.ParseComment(comment, nil) assert.Error(t, err) @@ -549,8 +1200,7 @@ func TestParseParamCommentByBodyTypeErr(t *testing.T) { func TestParseParamCommentByFormDataType(t *testing.T) { comment := `@Param file formData file true "this is a test file"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -572,8 +1222,7 @@ func TestParseParamCommentByFormDataType(t *testing.T) { func TestParseParamCommentByFormDataTypeUint64(t *testing.T) { comment := `@Param file formData uint64 true "this is a test file"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -595,7 +1244,7 @@ func TestParseParamCommentByFormDataTypeUint64(t *testing.T) { func TestParseParamCommentByNotSupportedType(t *testing.T) { comment := `@Param some_id not_supported int true "Some ID"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) @@ -603,7 +1252,7 @@ func TestParseParamCommentByNotSupportedType(t *testing.T) { func TestParseParamCommentNotMatch(t *testing.T) { comment := `@Param some_id body mock true` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) @@ -611,7 +1260,7 @@ func TestParseParamCommentNotMatch(t *testing.T) { func TestParseParamCommentByEnums(t *testing.T) { comment := `@Param some_id query string true "Some ID" Enums(A, B, C)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -635,7 +1284,7 @@ func TestParseParamCommentByEnums(t *testing.T) { assert.Equal(t, expected, string(b)) comment = `@Param some_id query int true "Some ID" Enums(1, 2, 3)` - operation = NewOperation() + operation = NewOperation(nil) err = operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -659,7 +1308,7 @@ func TestParseParamCommentByEnums(t *testing.T) { assert.Equal(t, expected, string(b)) comment = `@Param some_id query number true "Some ID" Enums(1.1, 2.2, 3.3)` - operation = NewOperation() + operation = NewOperation(nil) err = operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -683,7 +1332,7 @@ func TestParseParamCommentByEnums(t *testing.T) { assert.Equal(t, expected, string(b)) comment = `@Param some_id query bool true "Some ID" Enums(true, false)` - operation = NewOperation() + operation = NewOperation(nil) err = operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -705,7 +1354,7 @@ func TestParseParamCommentByEnums(t *testing.T) { }` assert.Equal(t, expected, string(b)) - operation = NewOperation() + operation = NewOperation(nil) comment = `@Param some_id query int true "Some ID" Enums(A, B, C)` assert.Error(t, operation.ParseComment(comment, nil)) @@ -722,7 +1371,7 @@ func TestParseParamCommentByEnums(t *testing.T) { func TestParseParamCommentByMaxLength(t *testing.T) { comment := `@Param some_id query string true "Some ID" MaxLength(10)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -750,7 +1399,7 @@ func TestParseParamCommentByMaxLength(t *testing.T) { func TestParseParamCommentByMinLength(t *testing.T) { comment := `@Param some_id query string true "Some ID" MinLength(10)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -776,9 +1425,9 @@ func TestParseParamCommentByMinLength(t *testing.T) { assert.Error(t, operation.ParseComment(comment, nil)) } -func TestParseParamCommentByMininum(t *testing.T) { - comment := `@Param some_id query int true "Some ID" Mininum(10)` - operation := NewOperation() +func TestParseParamCommentByMinimum(t *testing.T) { + comment := `@Param some_id query int true "Some ID" Minimum(10)` + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -797,16 +1446,19 @@ func TestParseParamCommentByMininum(t *testing.T) { }` assert.Equal(t, expected, string(b)) - comment = `@Param some_id query string true "Some ID" Mininum(10)` + 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" Mininum(Goopher)` + comment = `@Param some_id query integer true "Some ID" Minimum(Goopher)` assert.Error(t, operation.ParseComment(comment, nil)) } -func TestParseParamCommentByMaxinum(t *testing.T) { - comment := `@Param some_id query int true "Some ID" Maxinum(10)` - operation := NewOperation() +func TestParseParamCommentByMaximum(t *testing.T) { + comment := `@Param some_id query int true "Some ID" Maximum(10)` + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -825,17 +1477,20 @@ func TestParseParamCommentByMaxinum(t *testing.T) { }` assert.Equal(t, expected, string(b)) - comment = `@Param some_id query string true "Some ID" Maxinum(10)` + 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" Maxinum(Goopher)` + comment = `@Param some_id query integer true "Some ID" Maximum(Goopher)` assert.Error(t, operation.ParseComment(comment, nil)) } func TestParseParamCommentByDefault(t *testing.T) { comment := `@Param some_id query int true "Some ID" Default(10)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -857,7 +1512,7 @@ func TestParseParamCommentByDefault(t *testing.T) { func TestParseIdComment(t *testing.T) { comment := `@Id myOperationId` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -884,8 +1539,8 @@ func TestFindTypeDefInvalidPkg(t *testing.T) { func TestParseSecurityComment(t *testing.T) { comment := `@Security OAuth2Implicit[read, write]` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) + err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -905,8 +1560,7 @@ func TestParseSecurityComment(t *testing.T) { func TestParseMultiDescription(t *testing.T) { comment := `@Description line one` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -927,8 +1581,7 @@ func TestParseMultiDescription(t *testing.T) { func TestParseSummary(t *testing.T) { comment := `@summary line one` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -940,8 +1593,7 @@ func TestParseSummary(t *testing.T) { func TestParseDeprecationDescription(t *testing.T) { comment := `@Deprecated` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -951,27 +1603,11 @@ func TestParseDeprecationDescription(t *testing.T) { } } -func TestRegisterSchemaType(t *testing.T) { - operation := NewOperation() - - fset := token.NewFileSet() - astFile, err := goparser.ParseFile(fset, "main.go", `package main - import "timer" -`, goparser.ParseComments) - - assert.NoError(t, err) - - operation.parser = New() - _, _, err = operation.registerSchemaType("timer.Location", astFile) - assert.Error(t, err) -} - func TestParseExtentions(t *testing.T) { // Fail if there are no args for attributes. { comment := `@x-amazon-apigateway-integration` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a value") @@ -980,8 +1616,7 @@ func TestParseExtentions(t *testing.T) { // Fail if args of attributes are broken. { comment := `@x-amazon-apigateway-integration ["broken"}]` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a valid json value") @@ -990,8 +1625,7 @@ func TestParseExtentions(t *testing.T) { // OK { comment := `@x-amazon-apigateway-integration {"uri": "${some_arn}", "passthroughBehavior": "when_no_match", "httpMethod": "POST", "type": "aws_proxy"}` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1007,4 +1641,60 @@ func TestParseExtentions(t *testing.T) { b, _ := json.MarshalIndent(operation, "", " ") assert.Equal(t, expected, string(b)) } + + // Test x-tagGroups + { + comment := `@x-tagGroups [{"name":"Natural Persons","tags":["Person","PersonRisk","PersonDocuments"]}]` + operation := NewOperation(nil) + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + expected := `{ + "x-tagGroups": [ + { + "name": "Natural Persons", + "tags": [ + "Person", + "PersonRisk", + "PersonDocuments" + ] + } + ] +}` + + b, _ := json.MarshalIndent(operation, "", " ") + assert.Equal(t, expected, string(b)) + } +} + +func TestParseCodeSamples(t *testing.T) { + t.Run("Find sample by file", func(t *testing.T) { + comment := `@x-codeSamples file` + operation := NewOperation(nil, SetCodeExampleFilesDirectory("testdata/code_examples")) + operation.Summary = "example" + + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "no error should be thrown") + + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "summary": "example", + "x-codeSamples": { + "lang": "JavaScript", + "source": "console.log('Hello World');" + } +}` + assert.Equal(t, expected, string(b)) + }) + + t.Run("Example file not found", func(t *testing.T) { + comment := `@x-codeSamples file` + operation := NewOperation(nil, SetCodeExampleFilesDirectory("testdata/code_examples")) + operation.Summary = "exampel" + + err := operation.ParseComment(comment, nil) + assert.Error(t, err, "error was expected, as file does not exist") + }) } diff --git a/packages.go b/packages.go new file mode 100644 index 000000000..29fceb9e2 --- /dev/null +++ b/packages.go @@ -0,0 +1,234 @@ +package swag + +import ( + "go/ast" + "go/token" + "strings" +) + +//PackagesDefinitions map[package import path]*PackageDefinitions +type PackagesDefinitions struct { + files map[*ast.File]*AstFileInfo + packages map[string]*PackageDefinitions + uniqueDefinitions map[string]*TypeSpecDef +} + +//NewPackagesDefinitions create object PackagesDefinitions +func NewPackagesDefinitions() *PackagesDefinitions { + return &PackagesDefinitions{ + files: make(map[*ast.File]*AstFileInfo), + packages: make(map[string]*PackageDefinitions), + uniqueDefinitions: make(map[string]*TypeSpecDef), + } +} + +//CollectAstFile collect ast.file +func (pkgs *PackagesDefinitions) CollectAstFile(packageDir, path string, astFile *ast.File) { + if pkgs.files == nil { + pkgs.files = make(map[*ast.File]*AstFileInfo) + } + + pkgs.files[astFile] = &AstFileInfo{ + File: astFile, + Path: path, + PackagePath: packageDir, + } + + if len(packageDir) == 0 { + return + } + + if pkgs.packages == nil { + pkgs.packages = make(map[string]*PackageDefinitions) + } + + if pd, ok := pkgs.packages[packageDir]; ok { + pd.Files[path] = astFile + } else { + pkgs.packages[packageDir] = &PackageDefinitions{ + Name: astFile.Name.Name, + Files: map[string]*ast.File{path: astFile}, + TypeDefinitions: make(map[string]*TypeSpecDef), + } + } +} + +//RangeFiles for range the collection of ast.File +func (pkgs *PackagesDefinitions) RangeFiles(handle func(filename string, file *ast.File) error) error { + for file, info := range pkgs.files { + if err := handle(info.Path, file); err != nil { + return err + } + } + return nil +} + +//ParseTypes parse types +//@Return parsed definitions +func (pkgs *PackagesDefinitions) ParseTypes() (map[*TypeSpecDef]*Schema, error) { + parsedSchemas := make(map[*TypeSpecDef]*Schema) + for astFile, info := range pkgs.files { + for _, astDeclaration := range astFile.Decls { + if generalDeclaration, ok := astDeclaration.(*ast.GenDecl); ok && generalDeclaration.Tok == token.TYPE { + for _, astSpec := range generalDeclaration.Specs { + if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { + typeSpecDef := &TypeSpecDef{ + PkgPath: info.PackagePath, + File: astFile, + TypeSpec: typeSpec, + } + + if idt, ok := typeSpec.Type.(*ast.Ident); ok && IsGolangPrimitiveType(idt.Name) { + parsedSchemas[typeSpecDef] = &Schema{ + PkgPath: typeSpecDef.PkgPath, + Name: astFile.Name.Name, + Schema: PrimitiveSchema(TransToValidSchemeType(idt.Name)), + } + } + + if pkgs.uniqueDefinitions == nil { + pkgs.uniqueDefinitions = make(map[string]*TypeSpecDef) + } + + fullName := typeSpecDef.FullName() + anotherTypeDef, ok := pkgs.uniqueDefinitions[fullName] + if ok { + if typeSpecDef.PkgPath == anotherTypeDef.PkgPath { + continue + } else { + delete(pkgs.uniqueDefinitions, fullName) + } + } else { + pkgs.uniqueDefinitions[fullName] = typeSpecDef + } + + pkgs.packages[typeSpecDef.PkgPath].TypeDefinitions[typeSpecDef.Name()] = typeSpecDef + } + } + } + } + } + return parsedSchemas, nil +} + +func (pkgs *PackagesDefinitions) findTypeSpec(pkgPath string, typeName string) *TypeSpecDef { + if pkgs.packages != nil { + if pd, ok := pkgs.packages[pkgPath]; ok { + if typeSpec, ok := pd.TypeDefinitions[typeName]; ok { + return typeSpec + } + } + } + return nil +} + +// findPackagePathFromImports finds out the package path of a package via ranging imports of a ast.File +// @pkg the name of the target package +// @file current ast.File in which to search imports +// @return the package path of a package of @pkg +func (pkgs *PackagesDefinitions) findPackagePathFromImports(pkg string, file *ast.File) string { + if file == nil { + return "" + } + + if strings.ContainsRune(pkg, '.') { + pkg = strings.Split(pkg, ".")[0] + } + + hasAnonymousPkg := false + + // prior to match named package + for _, imp := range file.Imports { + if imp.Name != nil { + if imp.Name.Name == pkg { + return strings.Trim(imp.Path.Value, `"`) + } else if imp.Name.Name == "_" { + hasAnonymousPkg = true + } + } else if pkgs.packages != nil { + path := strings.Trim(imp.Path.Value, `"`) + if pd, ok := pkgs.packages[path]; ok { + if pd.Name == pkg { + return path + } + } + } + } + + //match unnamed package + if hasAnonymousPkg && pkgs.packages != nil { + for _, imp := range file.Imports { + if imp.Name == nil { + continue + } + if imp.Name.Name == "_" { + path := strings.Trim(imp.Path.Value, `"`) + if pd, ok := pkgs.packages[path]; ok { + if pd.Name == pkg { + return path + } + } + } + } + } + return "" +} + +// FindTypeSpec finds out TypeSpecDef of a type by typeName +// @typeName the name of the target type, if it starts with a package name, find its own package path from imports on top of @file +// @file the ast.file in which @typeName is used +// @pkgPath the package path of @file +func (pkgs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File) *TypeSpecDef { + if IsGolangPrimitiveType(typeName) { + return nil + } else if file == nil { // for test + return pkgs.uniqueDefinitions[typeName] + } + + if strings.ContainsRune(typeName, '.') { + parts := strings.Split(typeName, ".") + + isAliasPkgName := func(file *ast.File, pkgName string) bool { + if file != nil && file.Imports != nil { + for _, pkg := range file.Imports { + if pkg.Name != nil && pkg.Name.Name == pkgName { + return true + } + } + } + + return false + } + + if !isAliasPkgName(file, parts[0]) { + if typeDef, ok := pkgs.uniqueDefinitions[typeName]; ok { + return typeDef + } + } + + pkgPath := pkgs.findPackagePathFromImports(parts[0], file) + if len(pkgPath) == 0 && parts[0] == file.Name.Name { + pkgPath = pkgs.files[file].PackagePath + } + return pkgs.findTypeSpec(pkgPath, parts[1]) + } + + if typeDef, ok := pkgs.uniqueDefinitions[fullTypeName(file.Name.Name, typeName)]; ok { + return typeDef + } + + if typeDef := pkgs.findTypeSpec(pkgs.files[file].PackagePath, typeName); typeDef != nil { + return typeDef + } + + for _, imp := range file.Imports { + if imp.Name != nil && imp.Name.Name == "." { + pkgPath := strings.Trim(imp.Path.Value, `"`) + if typeDef := pkgs.findTypeSpec(pkgPath, typeName); typeDef != nil { + return typeDef + } + } + } + + return nil +} diff --git a/parser.go b/parser.go index 16f4fb96c..856cdf800 100644 --- a/parser.go +++ b/parser.go @@ -2,6 +2,7 @@ package swag import ( "encoding/json" + "errors" "fmt" "go/ast" "go/build" @@ -9,9 +10,9 @@ import ( "go/token" "io/ioutil" "net/http" + "net/url" "os" "os/exec" - "path" "path/filepath" "reflect" "sort" @@ -20,7 +21,6 @@ import ( "unicode" "github.com/KyleBanks/depth" - "github.com/go-openapi/jsonreference" "github.com/go-openapi/spec" ) @@ -35,25 +35,39 @@ const ( SnakeCase = "snakecase" ) +var ( + //ErrRecursiveParseStruct recursively parsing struct + ErrRecursiveParseStruct = errors.New("recursively parsing struct") + + //ErrFuncTypeField field type is func + ErrFuncTypeField = errors.New("field type is func") + + // ErrFailedConvertPrimitiveType Failed to convert for swag to interpretable type + ErrFailedConvertPrimitiveType = errors.New("swag property: failed convert primitive type") +) + // Parser implements a parser for Go source files. type Parser struct { // swagger represents the root document object for the API specification swagger *spec.Swagger - // files is a map that stores map[real_go_file_path][astFile] - files map[string]*ast.File + //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 - // TypeDefinitions is a map that stores [package name][type name][*ast.TypeSpec] - TypeDefinitions map[string]map[string]*ast.TypeSpec + //outputSchemas store schemas which will be export to swagger + outputSchemas map[*TypeSpecDef]*Schema - // ImportAliases is map that stores [import name][import package name][*ast.ImportSpec] - ImportAliases map[string]map[string]*ast.ImportSpec + //existSchemaNames store names of models for conflict determination + existSchemaNames map[string]*Schema - // CustomPrimitiveTypes is a map that stores custom primitive types to actual golang types [type name][string] - CustomPrimitiveTypes map[string]string + //toBeRenamedSchemas names of models to be renamed + toBeRenamedSchemas map[string]string - // registerTypes is a map that stores [refTypeName][*ast.TypeSpec] - registerTypes map[string]*ast.TypeSpec + //toBeRenamedSchemas URLs of ref models to be renamed + toBeRenamedRefURLs []*url.URL PropNamingStrategy string @@ -62,11 +76,23 @@ type Parser struct { // ParseDependencies whether swag should be parse outside dependency folder ParseDependency bool + // ParseInternal whether swag should parse internal packages + ParseInternal bool + // structStack stores full names of the structures that were already parsed or are being parsed now - structStack []string + structStack []*TypeSpecDef // markdownFileDir holds the path to the folder, where markdown files are stored markdownFileDir string + + // codeExampleFilesDir holds path to the folder, where code example files are stored + codeExampleFilesDir string + + // collectionFormatInQuery set the default collectionFormat otherwise then 'csv' for array in query params + collectionFormatInQuery string + + // excludes excludes dirs and files in SearchDir + excludes map[string]bool } // New creates a new Parser with default properties. @@ -77,7 +103,10 @@ func New(options ...func(*Parser)) *Parser { Info: &spec.Info{ InfoProps: spec.InfoProps{ Contact: &spec.ContactInfo{}, - License: &spec.License{}, + License: nil, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{}, }, }, Paths: &spec.Paths{ @@ -86,11 +115,12 @@ func New(options ...func(*Parser)) *Parser { Definitions: make(map[string]spec.Schema), }, }, - files: make(map[string]*ast.File), - TypeDefinitions: make(map[string]map[string]*ast.TypeSpec), - ImportAliases: make(map[string]map[string]*ast.ImportSpec), - CustomPrimitiveTypes: make(map[string]string), - registerTypes: make(map[string]*ast.TypeSpec), + packages: NewPackagesDefinitions(), + parsedSchemas: make(map[*TypeSpecDef]*Schema), + outputSchemas: make(map[*TypeSpecDef]*Schema), + existSchemaNames: make(map[string]*Schema), + toBeRenamedSchemas: make(map[string]string), + excludes: make(map[string]bool), } for _, option := range options { @@ -107,15 +137,38 @@ func SetMarkdownFileDirectory(directoryPath string) func(*Parser) { } } +// SetCodeExamplesDirectory sets the directory to search for code example files +func SetCodeExamplesDirectory(directoryPath string) func(*Parser) { + return func(p *Parser) { + p.codeExampleFilesDir = directoryPath + } +} + +// SetExcludedDirsAndFiles sets directories and files to be excluded when searching +func SetExcludedDirsAndFiles(excludes string) func(*Parser) { + return func(p *Parser) { + for _, f := range strings.Split(excludes, ",") { + f = strings.TrimSpace(f) + if f != "" { + f = filepath.Clean(f) + p.excludes[f] = true + } + } + } +} + // ParseAPI parses general api info for given searchDir and mainAPIFile -func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string) error { +func (parser *Parser) ParseAPI(searchDir, mainAPIFile string, parseDepth int) error { Printf("Generate general API Info, search dir:%s", searchDir) - if err := parser.getAllGoFileInfo(searchDir); err != nil { - return err + packageDir, err := getPkgName(searchDir) + if err != nil { + Printf("warning: failed to get package name in dir: %s, error: %s", searchDir, err.Error()) } - var t depth.Tree + if err = parser.getAllGoFileInfo(packageDir, searchDir); err != nil { + return err + } absMainAPIFilePath, err := filepath.Abs(filepath.Join(searchDir, mainAPIFile)) if err != nil { @@ -123,13 +176,19 @@ func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string) error { } if parser.ParseDependency { - pkgName, err := getPkgName(path.Dir(absMainAPIFilePath)) + var t depth.Tree + t.ResolveInternal = true + t.MaxDepth = parseDepth + + pkgName, err := getPkgName(filepath.Dir(absMainAPIFilePath)) if err != nil { return err } + if err := t.Resolve(pkgName); err != nil { return fmt.Errorf("pkg %s cannot find all dependencies, %s", pkgName, err) } + for i := 0; i < len(t.Root.Deps); i++ { if err := parser.getAllGoFileInfoFromDeps(&t.Root.Deps[i]); err != nil { return err @@ -137,21 +196,22 @@ func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string) error { } } - if err := parser.ParseGeneralAPIInfo(absMainAPIFilePath); err != nil { + if err = parser.ParseGeneralAPIInfo(absMainAPIFilePath); err != nil { return err } - for _, astFile := range parser.files { - parser.ParseType(astFile) + parser.parsedSchemas, err = parser.packages.ParseTypes() + if err != nil { + return err } - for fileName, astFile := range parser.files { - if err := parser.ParseRouterAPIInfo(fileName, astFile); err != nil { - return err - } + if err = parser.packages.RangeFiles(parser.ParseRouterAPIInfo); err != nil { + return err } - return parser.parseDefinitions() + parser.renameRefSchemas() + + return parser.checkOperationIDUniqueness() } func getPkgName(searchDir string) (string, error) { @@ -176,6 +236,14 @@ func getPkgName(searchDir string) (string, error) { return outStr, nil } +func initIfEmpty(license *spec.License) *spec.License { + if license == nil { + return new(spec.License) + } + + return license +} + // ParseGeneralAPIInfo parses general api info for given mainAPIFile path func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error { fileSet := token.NewFileSet() @@ -227,8 +295,10 @@ func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error { case "@contact.url": parser.swagger.Info.Contact.URL = value case "@license.name": + parser.swagger.Info.License = initIfEmpty(parser.swagger.Info.License) parser.swagger.Info.License.Name = value case "@license.url": + parser.swagger.Info.License = initIfEmpty(parser.swagger.Info.License) parser.swagger.Info.License.URL = value case "@host": parser.swagger.Host = value @@ -270,36 +340,52 @@ func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error { case "@securitydefinitions.basic": securityMap[value] = spec.BasicAuth() case "@securitydefinitions.apikey": - attrMap, _, err := extractSecurityAttribute(attribute, []string{"@in", "@name"}, comments[i+1:]) + attrMap, _, _, err := extractSecurityAttribute(attribute, []string{"@in", "@name"}, comments[i+1:]) if err != nil { return err } securityMap[value] = spec.APIKeyAuth(attrMap["@name"], attrMap["@in"]) case "@securitydefinitions.oauth2.application": - attrMap, scopes, err := extractSecurityAttribute(attribute, []string{"@tokenurl"}, comments[i+1:]) + attrMap, scopes, extensions, err := extractSecurityAttribute(attribute, []string{"@tokenurl"}, comments[i+1:]) if err != nil { return err } - securityMap[value] = securitySchemeOAuth2Application(attrMap["@tokenurl"], scopes) + securityMap[value] = securitySchemeOAuth2Application(attrMap["@tokenurl"], scopes, extensions) case "@securitydefinitions.oauth2.implicit": - attrMap, scopes, err := extractSecurityAttribute(attribute, []string{"@authorizationurl"}, comments[i+1:]) + attrMap, scopes, extensions, err := extractSecurityAttribute(attribute, []string{"@authorizationurl"}, comments[i+1:]) if err != nil { return err } - securityMap[value] = securitySchemeOAuth2Implicit(attrMap["@authorizationurl"], scopes) + securityMap[value] = securitySchemeOAuth2Implicit(attrMap["@authorizationurl"], scopes, extensions) case "@securitydefinitions.oauth2.password": - attrMap, scopes, err := extractSecurityAttribute(attribute, []string{"@tokenurl"}, comments[i+1:]) + attrMap, scopes, extensions, err := extractSecurityAttribute(attribute, []string{"@tokenurl"}, comments[i+1:]) if err != nil { return err } - securityMap[value] = securitySchemeOAuth2Password(attrMap["@tokenurl"], scopes) + securityMap[value] = securitySchemeOAuth2Password(attrMap["@tokenurl"], scopes, extensions) case "@securitydefinitions.oauth2.accesscode": - attrMap, scopes, err := extractSecurityAttribute(attribute, []string{"@tokenurl", "@authorizationurl"}, comments[i+1:]) + attrMap, scopes, extensions, err := extractSecurityAttribute(attribute, []string{"@tokenurl", "@authorizationurl"}, comments[i+1:]) if err != nil { return err } - securityMap[value] = securitySchemeOAuth2AccessToken(attrMap["@authorizationurl"], attrMap["@tokenurl"], scopes) + securityMap[value] = securitySchemeOAuth2AccessToken(attrMap["@authorizationurl"], attrMap["@tokenurl"], scopes, extensions) + case "@x-tokenname": + // ignore this + break + case "@query.collection.format": + parser.collectionFormatInQuery = 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", originalAttribute) + } + 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: prefixExtension := "@x-" if len(attribute) > 5 { // Prefix extension + 1 char + 1 space + 1 char @@ -313,7 +399,12 @@ func (parser *Parser) ParseGeneralAPIInfo(mainAPIFile string) error { if err := json.Unmarshal([]byte(split[1]), &valueJSON); err != nil { return fmt.Errorf("annotation %s need a valid json value", attribute) } - parser.swagger.AddExtension(extensionName, valueJSON) + + if strings.Contains(extensionName, "logo") { + parser.swagger.Info.Extensions.Add(extensionName, valueJSON) + } else { + parser.swagger.AddExtension(extensionName, valueJSON) + } } } } @@ -333,16 +424,17 @@ func isGeneralAPIComment(comment *ast.CommentGroup) bool { attribute := strings.ToLower(strings.Split(commentLine, " ")[0]) switch attribute { // The @summary, @router, @success,@failure annotation belongs to Operation - case "@summary", "@router", "@success", "@failure": + case "@summary", "@router", "@success", "@failure", "@response": return false } } return true } -func extractSecurityAttribute(context string, search []string, lines []string) (map[string]string, map[string]string, error) { +func extractSecurityAttribute(context string, search []string, lines []string) (map[string]string, map[string]string, map[string]interface{}, error) { attrMap := map[string]string{} scopes := map[string]string{} + extensions := map[string]interface{}{} for _, v := range lines { securityAttr := strings.ToLower(strings.Split(v, " ")[0]) for _, findterm := range search { @@ -353,58 +445,76 @@ func extractSecurityAttribute(context string, search []string, lines []string) ( } isExists, err := isExistsScope(securityAttr) if err != nil { - return nil, nil, err + return nil, nil, nil, err } if isExists { scopScheme, err := getScopeScheme(securityAttr) if err != nil { - return nil, nil, err + return nil, nil, nil, err } scopes[scopScheme] = v[len(securityAttr):] } + if securityAttr == "@x-tokenname" { + extensions["x-tokenName"] = strings.TrimSpace(v[len(securityAttr):]) + } // next securityDefinitions if strings.Index(securityAttr, "@securitydefinitions.") == 0 { break } } if len(attrMap) != len(search) { - return nil, nil, fmt.Errorf("%s is %v required", context, search) + return nil, nil, nil, fmt.Errorf("%s is %v required", context, search) } - return attrMap, scopes, nil + return attrMap, scopes, extensions, nil } -func securitySchemeOAuth2Application(tokenurl string, scopes map[string]string) *spec.SecurityScheme { +func securitySchemeOAuth2Application(tokenurl string, scopes map[string]string, extensions map[string]interface{}) *spec.SecurityScheme { securityScheme := spec.OAuth2Application(tokenurl) + securityScheme.VendorExtensible.Extensions = handleSecuritySchemaExtensions(extensions) for scope, description := range scopes { securityScheme.AddScope(scope, description) } return securityScheme } -func securitySchemeOAuth2Implicit(authorizationurl string, scopes map[string]string) *spec.SecurityScheme { +func securitySchemeOAuth2Implicit(authorizationurl string, scopes map[string]string, extensions map[string]interface{}) *spec.SecurityScheme { securityScheme := spec.OAuth2Implicit(authorizationurl) + securityScheme.VendorExtensible.Extensions = handleSecuritySchemaExtensions(extensions) for scope, description := range scopes { securityScheme.AddScope(scope, description) } return securityScheme } -func securitySchemeOAuth2Password(tokenurl string, scopes map[string]string) *spec.SecurityScheme { +func securitySchemeOAuth2Password(tokenurl string, scopes map[string]string, extensions map[string]interface{}) *spec.SecurityScheme { securityScheme := spec.OAuth2Password(tokenurl) + securityScheme.VendorExtensible.Extensions = handleSecuritySchemaExtensions(extensions) for scope, description := range scopes { securityScheme.AddScope(scope, description) } return securityScheme } -func securitySchemeOAuth2AccessToken(authorizationurl, tokenurl string, scopes map[string]string) *spec.SecurityScheme { +func securitySchemeOAuth2AccessToken(authorizationurl, tokenurl string, scopes map[string]string, extensions map[string]interface{}) *spec.SecurityScheme { securityScheme := spec.OAuth2AccessToken(authorizationurl, tokenurl) + securityScheme.VendorExtensible.Extensions = handleSecuritySchemaExtensions(extensions) for scope, description := range scopes { securityScheme.AddScope(scope, description) } return securityScheme } +func handleSecuritySchemaExtensions(providedExtensions map[string]interface{}) spec.Extensions { + var extensions spec.Extensions + if len(providedExtensions) > 0 { + extensions = make(map[string]interface{}, len(providedExtensions)) + for key, value := range providedExtensions { + extensions[key] = value + } + } + return extensions +} + func getMarkdownForTag(tagName string, dirPath string) ([]byte, error) { filesInfos, err := ioutil.ReadDir(dirPath) if err != nil { @@ -465,8 +575,7 @@ func (parser *Parser) ParseRouterAPIInfo(fileName string, astFile *ast.File) err switch astDeclaration := astDescription.(type) { case *ast.FuncDecl: if astDeclaration.Doc != nil && astDeclaration.Doc.List != nil { - operation := NewOperation() //for per 'function' comment, create a new 'Operation' object - operation.parser = parser + operation := NewOperation(parser, SetCodeExampleFilesDirectory(parser.codeExampleFilesDir)) //for per 'function' comment, create a new 'Operation' object for _, comment := range astDeclaration.Doc.List { if err := operation.ParseComment(comment.Text, astFile); err != nil { return fmt.Errorf("ParseComment error in file %s :%+v", fileName, err) @@ -503,141 +612,158 @@ func (parser *Parser) ParseRouterAPIInfo(fileName string, astFile *ast.File) err return nil } -// ParseType parses type info for given astFile. -func (parser *Parser) ParseType(astFile *ast.File) { - if _, ok := parser.TypeDefinitions[astFile.Name.String()]; !ok { - parser.TypeDefinitions[astFile.Name.String()] = make(map[string]*ast.TypeSpec) - } - - for _, astDeclaration := range astFile.Decls { - if generalDeclaration, ok := astDeclaration.(*ast.GenDecl); ok && generalDeclaration.Tok == token.TYPE { - for _, astSpec := range generalDeclaration.Specs { - if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { - typeName := fmt.Sprintf("%v", typeSpec.Type) - // check if its a custom primitive type - if IsGolangPrimitiveType(typeName) { - var typeSpecFullName = fmt.Sprintf("%s.%s", astFile.Name.String(), typeSpec.Name.String()) - parser.CustomPrimitiveTypes[typeSpecFullName] = TransToValidSchemeType(typeName) - } else { - parser.TypeDefinitions[astFile.Name.String()][typeSpec.Name.String()] = typeSpec - } +func convertFromSpecificToPrimitive(typeName string) (string, error) { + name := typeName + if strings.ContainsRune(name, '.') { + name = strings.Split(name, ".")[1] + } + switch strings.ToUpper(name) { + case "TIME", "OBJECTID", "UUID": + return STRING, nil + case "DECIMAL": + return NUMBER, nil + } + return typeName, ErrFailedConvertPrimitiveType +} - } +func (parser *Parser) getTypeSchema(typeName string, file *ast.File, ref bool) (*spec.Schema, error) { + if IsGolangPrimitiveType(typeName) { + return PrimitiveSchema(TransToValidSchemeType(typeName)), nil + } + + if schemaType, err := convertFromSpecificToPrimitive(typeName); err == nil { + return PrimitiveSchema(schemaType), nil + } + + typeSpecDef := parser.packages.FindTypeSpec(typeName, file) + if typeSpecDef == nil { + return nil, fmt.Errorf("cannot find type definition: %s", typeName) + } + + schema, ok := parser.parsedSchemas[typeSpecDef] + if !ok { + var err error + schema, err = parser.ParseDefinition(typeSpecDef) + if err == ErrRecursiveParseStruct { + if ref { + return parser.getRefTypeSchema(typeSpecDef, schema), nil } + + } else if err != nil { + return nil, err } } - for _, importSpec := range astFile.Imports { - if importSpec.Name == nil { - continue - } + if ref && len(schema.Schema.Type) > 0 && schema.Schema.Type[0] == OBJECT { + return parser.getRefTypeSchema(typeSpecDef, schema), nil + } + return schema.Schema, nil +} - alias := importSpec.Name.Name +func (parser *Parser) renameRefSchemas() { + if len(parser.toBeRenamedSchemas) == 0 { + return + } - if _, ok := parser.ImportAliases[alias]; !ok { - parser.ImportAliases[alias] = make(map[string]*ast.ImportSpec) + //rename schemas in swagger.Definitions + for name, pkgPath := range parser.toBeRenamedSchemas { + if schema, ok := parser.swagger.Definitions[name]; ok { + delete(parser.swagger.Definitions, name) + name = parser.renameSchema(name, pkgPath) + parser.swagger.Definitions[name] = schema } + } - importParts := strings.Split(strings.Trim(importSpec.Path.Value, "\""), "/") - importPkgName := importParts[len(importParts)-1] - - parser.ImportAliases[alias][importPkgName] = importSpec + //rename URLs if match + for _, url := range parser.toBeRenamedRefURLs { + parts := strings.Split(url.Fragment, "/") + name := parts[len(parts)-1] + if pkgPath, ok := parser.toBeRenamedSchemas[name]; ok { + parts[len(parts)-1] = parser.renameSchema(name, pkgPath) + url.Fragment = strings.Join(parts, "/") + } } } -func (parser *Parser) isInStructStack(refTypeName string) bool { - for _, structName := range parser.structStack { - if refTypeName == structName { - return true +func (parser *Parser) renameSchema(name, pkgPath string) string { + parts := strings.Split(name, ".") + name = fullTypeName(pkgPath, parts[len(parts)-1]) + name = strings.ReplaceAll(name, "/", "_") + return name +} + +func (parser *Parser) getRefTypeSchema(typeSpecDef *TypeSpecDef, schema *Schema) *spec.Schema { + if _, ok := parser.outputSchemas[typeSpecDef]; !ok { + if existSchema, ok := parser.existSchemaNames[schema.Name]; ok { + //store the first one to be renamed after parsing over + if _, ok = parser.toBeRenamedSchemas[existSchema.Name]; !ok { + parser.toBeRenamedSchemas[existSchema.Name] = existSchema.PkgPath + } + //rename not the first one + schema.Name = parser.renameSchema(schema.Name, schema.PkgPath) + } else { + parser.existSchemaNames[schema.Name] = schema + } + if schema.Schema != nil { + parser.swagger.Definitions[schema.Name] = *schema.Schema + } else { + parser.swagger.Definitions[schema.Name] = spec.Schema{} } + parser.outputSchemas[typeSpecDef] = schema } - return false + + refSchema := RefSchema(schema.Name) + //store every URL + parser.toBeRenamedRefURLs = append(parser.toBeRenamedRefURLs, refSchema.Ref.Ref.GetURL()) + return refSchema } -// parseDefinitions parses Swagger Api definitions. -func (parser *Parser) parseDefinitions() error { - // sort the typeNames so that parsing definitions is deterministic - typeNames := make([]string, 0, len(parser.registerTypes)) - for refTypeName := range parser.registerTypes { - typeNames = append(typeNames, refTypeName) - } - sort.Strings(typeNames) - - for _, refTypeName := range typeNames { - typeSpec := parser.registerTypes[refTypeName] - ss := strings.Split(refTypeName, ".") - pkgName := ss[0] - parser.structStack = nil - if err := parser.ParseDefinition(pkgName, typeSpec.Name.Name, typeSpec); err != nil { - return err +func (parser *Parser) isInStructStack(typeSpecDef *TypeSpecDef) bool { + for _, specDef := range parser.structStack { + if typeSpecDef == specDef { + return true } } - return nil + return false } // ParseDefinition 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 (parser *Parser) ParseDefinition(pkgName, typeName string, typeSpec *ast.TypeSpec) error { - refTypeName := fullTypeName(pkgName, typeName) +func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) { + typeName := typeSpecDef.FullName() + refTypeName := TypeDocName(typeName, typeSpecDef.TypeSpec) - if typeSpec == nil { - Println("Skipping '" + refTypeName + "', pkg '" + pkgName + "' not found, try add flag --parseDependency or --parseVendor.") - return nil + if schema, ok := parser.parsedSchemas[typeSpecDef]; ok { + Println("Skipping '" + typeName + "', already parsed.") + return schema, nil } - if _, isParsed := parser.swagger.Definitions[refTypeName]; isParsed { - Println("Skipping '" + refTypeName + "', already parsed.") - return nil + if parser.isInStructStack(typeSpecDef) { + Println("Skipping '" + typeName + "', recursion detected.") + return &Schema{ + Name: refTypeName, + PkgPath: typeSpecDef.PkgPath, + Schema: PrimitiveSchema(OBJECT)}, + ErrRecursiveParseStruct } + parser.structStack = append(parser.structStack, typeSpecDef) - if parser.isInStructStack(refTypeName) { - Println("Skipping '" + refTypeName + "', recursion detected.") - return nil - } - parser.structStack = append(parser.structStack, refTypeName) + Println("Generating " + typeName) - Println("Generating " + refTypeName) - - schema, err := parser.parseTypeExpr(pkgName, typeName, typeSpec.Type) + schema, err := parser.parseTypeExpr(typeSpecDef.File, typeSpecDef.TypeSpec.Type, false) if err != nil { - return err - } - parser.swagger.Definitions[refTypeName] = *schema - return nil -} - -func (parser *Parser) collectRequiredFields(pkgName string, properties map[string]spec.Schema, extraRequired []string) (requiredFields []string) { - // created sorted list of properties keys so when we iterate over them it's deterministic - ks := make([]string, 0, len(properties)) - for k := range properties { - ks = append(ks, k) - } - sort.Strings(ks) - - requiredFields = make([]string, 0) - - // iterate over keys list instead of map to avoid the random shuffle of the order that go does for maps - for _, k := range ks { - prop := properties[k] - - // todo find the pkgName of the property type - tname := prop.SchemaProps.Type[0] - if _, ok := parser.TypeDefinitions[pkgName][tname]; ok { - tspec := parser.TypeDefinitions[pkgName][tname] - parser.ParseDefinition(pkgName, tname, tspec) - } - requiredFields = append(requiredFields, prop.SchemaProps.Required...) - properties[k] = prop + return nil, err } + s := &Schema{Name: refTypeName, PkgPath: typeSpecDef.PkgPath, Schema: schema} + parser.parsedSchemas[typeSpecDef] = s - if extraRequired != nil { - requiredFields = append(requiredFields, extraRequired...) + //update an empty schema as a result of recursion + if s2, ok := parser.outputSchemas[typeSpecDef]; ok { + parser.swagger.Definitions[s2.Name] = *schema } - sort.Strings(requiredFields) - - return + return s, nil } func fullTypeName(pkgName, typeName string) string { @@ -649,123 +775,76 @@ func fullTypeName(pkgName, typeName string) string { // parseTypeExpr parses given type expression that corresponds to the type under // given name and package, and returns swagger schema for it. -func (parser *Parser) parseTypeExpr(pkgName, typeName string, typeExpr ast.Expr) (*spec.Schema, error) { - +func (parser *Parser) parseTypeExpr(file *ast.File, typeExpr ast.Expr, ref bool) (*spec.Schema, error) { switch expr := typeExpr.(type) { // type Foo struct {...} case *ast.StructType: - refTypeName := fullTypeName(pkgName, typeName) - if schema, isParsed := parser.swagger.Definitions[refTypeName]; isParsed { - return &schema, nil - } - - return parser.parseStruct(pkgName, expr.Fields) + return parser.parseStruct(file, expr.Fields) // type Foo Baz case *ast.Ident: - if IsGolangPrimitiveType(expr.Name) { - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: spec.StringOrArray{TransToValidSchemeType(expr.Name)}, - }, - }, nil - } - refTypeName := fullTypeName(pkgName, expr.Name) - if _, isParsed := parser.swagger.Definitions[refTypeName]; !isParsed { - if typedef, ok := parser.TypeDefinitions[pkgName][expr.Name]; ok { - parser.ParseDefinition(pkgName, expr.Name, typedef) - } - } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Ref: spec.Ref{ - Ref: jsonreference.MustCreateRef("#/definitions/" + refTypeName), - }, - }, - }, nil + return parser.getTypeSchema(expr.Name, file, ref) // type Foo *Baz case *ast.StarExpr: - return parser.parseTypeExpr(pkgName, typeName, expr.X) - - // type Foo []Baz - case *ast.ArrayType: - itemSchema, err := parser.parseTypeExpr(pkgName, "", expr.Elt) - if err != nil { - return &spec.Schema{}, err - } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: itemSchema, - }, - }, - }, nil + return parser.parseTypeExpr(file, expr.X, ref) // type Foo pkg.Bar case *ast.SelectorExpr: if xIdent, ok := expr.X.(*ast.Ident); ok { - return parser.parseTypeExpr(xIdent.Name, expr.Sel.Name, expr.Sel) + return parser.getTypeSchema(fullTypeName(xIdent.Name, expr.Sel.Name), file, ref) } - + // type Foo []Baz + case *ast.ArrayType: + itemSchema, err := parser.parseTypeExpr(file, expr.Elt, true) + if err != nil { + return nil, err + } + return spec.ArrayProperty(itemSchema), nil // type Foo map[string]Bar case *ast.MapType: - var valueSchema spec.SchemaOrBool if _, ok := expr.Value.(*ast.InterfaceType); ok { - valueSchema.Allows = true - } else { - schema, err := parser.parseTypeExpr(pkgName, "", expr.Value) - if err != nil { - return &spec.Schema{}, err - } - valueSchema.Schema = schema + return spec.MapProperty(nil), nil } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - AdditionalProperties: &valueSchema, - }, - }, nil + schema, err := parser.parseTypeExpr(file, expr.Value, true) + if err != nil { + return nil, err + } + return spec.MapProperty(schema), nil + case *ast.FuncType: + return nil, ErrFuncTypeField // ... default: Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - }, - }, nil + return PrimitiveSchema(OBJECT), nil } -func (parser *Parser) parseStruct(pkgName string, fields *ast.FieldList) (*spec.Schema, error) { +func (parser *Parser) parseStruct(file *ast.File, fields *ast.FieldList) (*spec.Schema, error) { - extraRequired := make([]string, 0) + required := make([]string, 0) properties := make(map[string]spec.Schema) for _, field := range fields.List { - fieldProps, requiredFromAnon, err := parser.parseStructField(pkgName, field) - if err != nil { - return &spec.Schema{}, err + fieldProps, requiredFromAnon, err := parser.parseStructField(file, field) + if err == ErrFuncTypeField { + continue + } else if err != nil { + return nil, err + } else if len(fieldProps) == 0 { + continue } - extraRequired = append(extraRequired, requiredFromAnon...) + required = append(required, requiredFromAnon...) for k, v := range fieldProps { properties[k] = v } } - // collect requireds from our properties and anonymous fields - required := parser.collectRequiredFields(pkgName, properties, extraRequired) - - // unset required from properties because we've collected them - for k, prop := range properties { - prop.SchemaProps.Required = make([]string, 0) - properties[k] = prop - } + sort.Strings(required) return &spec.Schema{ SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, + Type: []string{OBJECT}, Properties: properties, Required: required, }}, nil @@ -790,322 +869,106 @@ type structField struct { extensions map[string]interface{} } -func (sf *structField) toStandardSchema() *spec.Schema { - required := make([]string, 0) - if sf.isRequired { - required = append(required, sf.name) - } - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{sf.schemaType}, - Description: sf.desc, - Format: sf.formatType, - Required: required, - Maximum: sf.maximum, - Minimum: sf.minimum, - MaxLength: sf.maxLength, - MinLength: sf.minLength, - Enum: sf.enums, - Default: sf.defaultValue, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - Example: sf.exampleValue, - ReadOnly: sf.readOnly, - }, - VendorExtensible: spec.VendorExtensible{ - Extensions: sf.extensions, - }, - } -} - -func (parser *Parser) parseStructField(pkgName string, field *ast.Field) (map[string]spec.Schema, []string, error) { - properties := map[string]spec.Schema{} - +func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[string]spec.Schema, []string, error) { if field.Names == nil { - fullTypeName, err := getFieldType(field.Type) - if err != nil { - return properties, []string{}, nil + 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 + } } - typeName := fullTypeName - - if splits := strings.Split(fullTypeName, "."); len(splits) > 1 { - pkgName = splits[0] - typeName = splits[1] + typeName, err := getFieldType(field.Type) + if err != nil { + return nil, nil, err } - - typeSpec := parser.TypeDefinitions[pkgName][typeName] - if typeSpec != nil { - schema, err := parser.parseTypeExpr(pkgName, typeName, typeSpec.Type) - if err != nil { - return properties, []string{}, err - } - schemaType := "unknown" - if len(schema.SchemaProps.Type) > 0 { - schemaType = schema.SchemaProps.Type[0] + schema, err := parser.getTypeSchema(typeName, file, false) + if err != nil { + return nil, nil, err + } + if len(schema.Type) > 0 && schema.Type[0] == OBJECT { + if len(schema.Properties) == 0 { + return nil, nil, nil } - switch schemaType { - case "object": - for k, v := range schema.SchemaProps.Properties { - properties[k] = v - } - case "array": - properties[typeName] = *schema - default: - Printf("Can't extract properties from a schema of type '%s'", schemaType) + properties := map[string]spec.Schema{} + for k, v := range schema.Properties { + properties[k] = v } return properties, schema.SchemaProps.Required, nil } - - return properties, nil, nil + //for alias type of non-struct types ,such as array,map, etc. ignore field tag. + return map[string]spec.Schema{typeName: *schema}, nil, nil } - structField, err := parser.parseField(pkgName, field) + fieldName, schema, err := parser.getFieldName(field) if err != nil { - return properties, nil, err - } - if structField.name == "" { - return properties, nil, nil + return nil, nil, err } - - // TODO: find package of schemaType and/or arrayType - if structField.crossPkg != "" { - pkgName = structField.crossPkg + if fieldName == "" { + return nil, nil, nil } - - fillObject := func(src, dest interface{}) error { - bin, err := json.Marshal(src) + if schema == nil { + typeName, err := getFieldType(field.Type) + if err == nil { + //named type + schema, err = parser.getTypeSchema(typeName, file, true) + } else { + //unnamed type + schema, err = parser.parseTypeExpr(file, field.Type, false) + } if err != nil { - return err + return nil, nil, err } - return json.Unmarshal(bin, dest) } - //for spec.Schema have implemented json.Marshaler, here in another way to convert - fillSchema := func(src, dest *spec.Schema) error { - err = fillObject(&src.SchemaProps, &dest.SchemaProps) - if err != nil { - return err - } - err = fillObject(&src.SwaggerSchemaProps, &dest.SwaggerSchemaProps) - if err != nil { - return err - } - return fillObject(&src.VendorExtensible, &dest.VendorExtensible) + types := parser.GetSchemaTypePath(schema, 2) + if len(types) == 0 { + return nil, nil, fmt.Errorf("invalid type for field: %s", field.Names[0]) } - if _, ok := parser.TypeDefinitions[pkgName][structField.schemaType]; ok { // user type field - // write definition if not yet present - parser.ParseDefinition(pkgName, structField.schemaType, - parser.TypeDefinitions[pkgName][structField.schemaType]) - required := make([]string, 0) - if structField.isRequired { - required = append(required, structField.name) - } - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, // to avoid swagger validation error - Description: structField.desc, - Required: required, - Ref: spec.Ref{ - Ref: jsonreference.MustCreateRef("#/definitions/" + pkgName + "." + structField.schemaType), - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } else if structField.schemaType == "array" { // array field type - // if defined -- ref it - if _, ok := parser.TypeDefinitions[pkgName][structField.arrayType]; ok { // user type in array - parser.ParseDefinition(pkgName, structField.arrayType, - parser.TypeDefinitions[pkgName][structField.arrayType]) - required := make([]string, 0) - if structField.isRequired { - required = append(required, structField.name) - } - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Required: required, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Ref: spec.Ref{ - Ref: jsonreference.MustCreateRef("#/definitions/" + pkgName + "." + structField.arrayType), - }, - }, - }, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } else if structField.arrayType == "object" { - // Anonymous struct - if astTypeArray, ok := field.Type.(*ast.ArrayType); ok { // if array - props := make(map[string]spec.Schema) - if expr, ok := astTypeArray.Elt.(*ast.StructType); ok { - for _, field := range expr.Fields.List { - var fieldProps map[string]spec.Schema - fieldProps, _, err = parser.parseStructField(pkgName, field) - if err != nil { - return properties, nil, err - } - for k, v := range fieldProps { - props[k] = v - } - } - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: props, - }, - }, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } else { - schema, _ := parser.parseTypeExpr(pkgName, "", astTypeArray.Elt) - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Items: &spec.SchemaOrArray{ - Schema: schema, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } - } - } else if structField.arrayType == "array" { - if astTypeArray, ok := field.Type.(*ast.ArrayType); ok { - schema, _ := parser.parseTypeExpr(pkgName, "", astTypeArray.Elt) - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Items: &spec.SchemaOrArray{ - Schema: schema, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } - } else { - // standard type in array - required := make([]string, 0) - if structField.isRequired { - required = append(required, structField.name) - } + structField, err := parser.parseFieldTag(field, types) + if err != nil { + return nil, nil, err + } - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Format: structField.formatType, - Required: required, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.arrayType}, - Maximum: structField.maximum, - Minimum: structField.minimum, - MaxLength: structField.maxLength, - MinLength: structField.minLength, - Enum: structField.enums, - Default: structField.defaultValue, - }, - }, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - Example: structField.exampleValue, - ReadOnly: structField.readOnly, - }, - } - } - } else if astTypeMap, ok := field.Type.(*ast.MapType); ok { // if map - stdSchema := structField.toStandardSchema() - mapValueSchema, err := parser.parseTypeExpr(pkgName, "", astTypeMap) - if err != nil { - return properties, nil, err - } - stdSchema.Type = mapValueSchema.Type - stdSchema.AdditionalProperties = mapValueSchema.AdditionalProperties - properties[structField.name] = *stdSchema - } else { - stdSchema := structField.toStandardSchema() - properties[structField.name] = *stdSchema - - if nestStar, ok := field.Type.(*ast.StarExpr); ok { - if !IsGolangPrimitiveType(structField.schemaType) { - schema, err := parser.parseTypeExpr(pkgName, structField.schemaType, nestStar.X) - if err != nil { - return properties, nil, err - } + if structField.schemaType == "string" && types[0] != structField.schemaType { + schema = PrimitiveSchema(structField.schemaType) + } - if len(schema.SchemaProps.Type) > 0 { - err = fillSchema(schema, stdSchema) - if err != nil { - return properties, nil, err - } - properties[structField.name] = *stdSchema - return properties, nil, nil - } - } - } else if nestStruct, ok := field.Type.(*ast.StructType); ok { - props := map[string]spec.Schema{} - nestRequired := make([]string, 0) - for _, v := range nestStruct.Fields.List { - p, _, err := parser.parseStructField(pkgName, v) - if err != nil { - return properties, nil, err - } - for k, v := range p { - if v.SchemaProps.Type[0] != "object" { - nestRequired = append(nestRequired, v.SchemaProps.Required...) - v.SchemaProps.Required = make([]string, 0) - } - props[k] = v - } - } - stdSchema.Properties = props - stdSchema.Required = nestRequired - properties[structField.name] = *stdSchema - } + schema.Description = structField.desc + schema.ReadOnly = structField.readOnly + schema.Default = structField.defaultValue + schema.Example = structField.exampleValue + schema.Format = structField.formatType + schema.Extensions = structField.extensions + eleSchema := schema + if structField.schemaType == "array" { + eleSchema = schema.Items.Schema } - return properties, nil, nil -} + eleSchema.Maximum = structField.maximum + eleSchema.Minimum = structField.minimum + eleSchema.MaxLength = structField.maxLength + eleSchema.MinLength = structField.minLength + eleSchema.Enum = structField.enums -func getFieldType(field interface{}) (string, error) { + var tagRequired []string + if structField.isRequired { + tagRequired = append(tagRequired, fieldName) + } + return map[string]spec.Schema{fieldName: *schema}, tagRequired, nil +} +func getFieldType(field ast.Expr) (string, error) { switch ftype := field.(type) { case *ast.Ident: return ftype.Name, nil - case *ast.SelectorExpr: packageName, err := getFieldType(ftype.X) if err != nil { return "", err } - return fmt.Sprintf("%s.%s", packageName, ftype.Sel.Name), nil + return fullTypeName(packageName, ftype.Sel.Name), nil case *ast.StarExpr: fullName, err := getFieldType(ftype.X) @@ -1113,48 +976,60 @@ func getFieldType(field interface{}) (string, error) { return "", err } return fullName, nil - } return "", fmt.Errorf("unknown field type %#v", field) } -func (parser *Parser) parseField(pkgName string, field *ast.Field) (*structField, error) { - prop, err := getPropertyName(pkgName, field.Type, parser) - if err != nil { - return nil, err +func (parser *Parser) getFieldName(field *ast.Field) (name string, schema *spec.Schema, err error) { + // Skip non-exported fields. + if !ast.IsExported(field.Names[0].Name) { + return "", nil, nil } - if len(prop.ArrayType) == 0 { - if err := CheckSchemaType(prop.SchemaType); err != nil { - return nil, err + if field.Tag != nil { + // `json:"tag"` -> json:"tag" + structTag := reflect.StructTag(strings.Replace(field.Tag.Value, "`", "", -1)) + if ignoreTag := structTag.Get("swaggerignore"); strings.EqualFold(ignoreTag, "true") { + return "", nil, nil } - } else { - if err := CheckSchemaType("array"); err != nil { - return nil, err + + name = structTag.Get("json") + // json:"tag,hoge" + if name = strings.TrimSpace(strings.Split(name, ",")[0]); name == "-" { + return "", nil, nil + } + + if typeTag := structTag.Get("swaggertype"); typeTag != "" { + parts := strings.Split(typeTag, ",") + schema, err = BuildCustomSchema(parts) + if err != nil { + return "", nil, err + } } } - // Skip func fields. - if prop.SchemaType == "func" { - return &structField{name: ""}, nil + if name == "" { + switch parser.PropNamingStrategy { + case SnakeCase: + name = toSnakeCase(field.Names[0].Name) + case PascalCase: + name = field.Names[0].Name + case CamelCase: + name = toLowerCamelCase(field.Names[0].Name) + default: + name = toLowerCamelCase(field.Names[0].Name) + } } + return name, schema, err +} +func (parser *Parser) parseFieldTag(field *ast.Field, types []string) (*structField, error) { structField := &structField{ - name: field.Names[0].Name, - schemaType: prop.SchemaType, - arrayType: prop.ArrayType, - crossPkg: prop.CrossPkg, - } - - switch parser.PropNamingStrategy { - case SnakeCase: - structField.name = toSnakeCase(structField.name) - case PascalCase: - //use struct field name - case CamelCase: - structField.name = toLowerCamelCase(structField.name) - default: - structField.name = toLowerCamelCase(structField.name) + // name: field.Names[0].Name, + schemaType: types[0], + } + if len(types) > 1 && (types[0] == "array" || types[0] == "object") { + structField.arrayType = types[1] } if field.Doc != nil { @@ -1169,54 +1044,23 @@ func (parser *Parser) parseField(pkgName string, field *ast.Field) (*structField } // `json:"tag"` -> json:"tag" structTag := reflect.StructTag(strings.Replace(field.Tag.Value, "`", "", -1)) - jsonTag := structTag.Get("json") - // json:"tag,hoge" - if strings.Contains(jsonTag, ",") { - // json:",hoge" - if strings.HasPrefix(jsonTag, ",") { - jsonTag = "" - } else { - jsonTag = strings.SplitN(jsonTag, ",", 2)[0] - } - } - if jsonTag == "-" { - structField.name = "" - } else if jsonTag != "" { - structField.name = jsonTag - } - if typeTag := structTag.Get("swaggertype"); typeTag != "" { - parts := strings.Split(typeTag, ",") - if 0 < len(parts) && len(parts) <= 2 { - newSchemaType := parts[0] - newArrayType := structField.arrayType - if len(parts) >= 2 { - if newSchemaType == "array" { - newArrayType = parts[1] - if err := CheckSchemaType(newArrayType); err != nil { - return nil, err - } - } else if newSchemaType == "primitive" { - newSchemaType = parts[1] - newArrayType = parts[1] - } - } + jsonTag := structTag.Get("json") + // json:"name,string" or json:",string" + hasStringTag := strings.Contains(jsonTag, ",string") - if err := CheckSchemaType(newSchemaType); err != nil { + if exampleTag := structTag.Get("example"); exampleTag != "" { + if hasStringTag { + // then the example must be in string format + structField.exampleValue = exampleTag + } else { + example, err := defineTypeOfExample(structField.schemaType, structField.arrayType, exampleTag) + if err != nil { return nil, err } - - structField.schemaType = newSchemaType - structField.arrayType = newArrayType + structField.exampleValue = example } } - if exampleTag := structTag.Get("example"); exampleTag != "" { - example, err := defineTypeOfExample(structField.schemaType, structField.arrayType, exampleTag) - if err != nil { - return nil, err - } - structField.exampleValue = example - } if formatTag := structTag.Get("format"); formatTag != "" { structField.formatType = formatTag } @@ -1249,7 +1093,7 @@ func (parser *Parser) parseField(pkgName string, field *ast.Field) (*structField } if enumsTag := structTag.Get("enums"); enumsTag != "" { enumType := structField.schemaType - if structField.schemaType == "array" { + if structField.schemaType == ARRAY { enumType = structField.arrayType } @@ -1282,7 +1126,7 @@ func (parser *Parser) parseField(pkgName string, field *ast.Field) (*structField } structField.minimum = minimum } - if structField.schemaType == "string" || structField.arrayType == "string" { + if structField.schemaType == STRING || structField.arrayType == STRING { maxLength, err := getIntTag(structTag, "maxLength") if err != nil { return nil, err @@ -1299,9 +1143,63 @@ func (parser *Parser) parseField(pkgName string, field *ast.Field) (*structField structField.readOnly = readOnly == "true" } + // perform this after setting everything else (min, max, etc...) + if hasStringTag { + + // @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", + } + + if defaultValue, ok := defaultValues[structField.schemaType]; ok { + structField.schemaType = STRING + + if structField.exampleValue == nil { + // if exampleValue is not defined by the user, + // we will force an example with a correct value + // (eg: int->"0", bool:"false") + structField.exampleValue = defaultValue + } + } + } + return structField, nil } +// GetSchemaTypePath get path of schema type +func (parser *Parser) GetSchemaTypePath(schema *spec.Schema, depth int) []string { + if schema == nil || depth == 0 { + return nil + } + if name := schema.Ref.String(); name != "" { + if pos := strings.LastIndexByte(name, '/'); pos >= 0 { + name = name[pos+1:] + if schema, ok := parser.swagger.Definitions[name]; ok { + return parser.GetSchemaTypePath(&schema, depth) + } + } + } else if len(schema.Type) > 0 { + if schema.Type[0] == "array" { + depth-- + s := []string{schema.Type[0]} + return append(s, parser.GetSchemaTypePath(schema.Items.Schema, depth)...) + } else if schema.Type[0] == OBJECT { + if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil { + // for map + depth-- + s := []string{schema.Type[0]} + return append(s, parser.GetSchemaTypePath(schema.AdditionalProperties.Schema, depth)...) + } + } + return []string{schema.Type[0]} + } + return nil +} + func replaceLastTag(slice []spec.Tag, element spec.Tag) { slice = slice[:len(slice)-1] slice = append(slice, element) @@ -1370,27 +1268,27 @@ func toLowerCamelCase(in string) string { // defineTypeOfExample example value define the type (object and array unsupported) func defineTypeOfExample(schemaType, arrayType, exampleValue string) (interface{}, error) { switch schemaType { - case "string": + case STRING: return exampleValue, nil - case "number": + case NUMBER: v, err := strconv.ParseFloat(exampleValue, 64) if err != nil { return nil, fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err) } return v, nil - case "integer": + case INTEGER: v, err := strconv.Atoi(exampleValue) if err != nil { return nil, fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err) } return v, nil - case "boolean": + case BOOLEAN: v, err := strconv.ParseBool(exampleValue) if err != nil { return nil, fmt.Errorf("example value %s can't convert to %s err: %s", exampleValue, schemaType, err) } return v, nil - case "array": + case ARRAY: values := strings.Split(exampleValue, ",") result := make([]interface{}, 0) for _, value := range values { @@ -1401,21 +1299,59 @@ func defineTypeOfExample(schemaType, arrayType, exampleValue string) (interface{ result = append(result, v) } return result, nil + case OBJECT: + if arrayType == "" { + return nil, fmt.Errorf("%s is unsupported type in example value", schemaType) + } + + values := strings.Split(exampleValue, ",") + result := map[string]interface{}{} + for _, value := range values { + mapData := strings.Split(value, ":") + + if len(mapData) == 2 { + v, err := defineTypeOfExample(arrayType, "", mapData[1]) + if err != nil { + return nil, err + } + result[mapData[0]] = v + } else { + return nil, fmt.Errorf("example value %s should format: key:value", exampleValue) + } + } + return result, nil default: return nil, fmt.Errorf("%s is unsupported type in example value", schemaType) } } // GetAllGoFileInfo gets all Go source files information for given searchDir. -func (parser *Parser) getAllGoFileInfo(searchDir string) error { - return filepath.Walk(searchDir, parser.visit) +func (parser *Parser) getAllGoFileInfo(packageDir, searchDir string) error { + return filepath.Walk(searchDir, func(path string, f os.FileInfo, err error) error { + if err := parser.Skip(path, f); err != nil { + return err + } else if f.IsDir() { + return nil + } + + relPath, err := filepath.Rel(searchDir, path) + if err != nil { + return err + } + return parser.parseFile(filepath.ToSlash(filepath.Dir(filepath.Clean(filepath.Join(packageDir, relPath)))), path, nil) + }) } func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg) error { - if pkg.Internal || !pkg.Resolved { // ignored internal and not resolved dependencies + ignoreInternal := pkg.Internal && !parser.ParseInternal + if ignoreInternal || !pkg.Resolved { // ignored internal and not resolved dependencies return nil } + // Skip cgo + if pkg.Raw == nil && pkg.Name == "C" { + return nil + } srcDir := pkg.Raw.Dir files, err := ioutil.ReadDir(srcDir) // only parsing files in the dir(don't contains sub dir files) if err != nil { @@ -1428,7 +1364,7 @@ func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg) error { } path := filepath.Join(srcDir, f.Name()) - if err := parser.parseFile(path); err != nil { + if err := parser.parseFile(pkg.Name, path, nil); err != nil { return err } } @@ -1442,44 +1378,84 @@ func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg) error { return nil } -func (parser *Parser) visit(path string, f os.FileInfo, err error) error { - if err := parser.Skip(path, f); err != nil { - return err +func (parser *Parser) parseFile(packageDir, path string, src interface{}) error { + if strings.HasSuffix(strings.ToLower(path), "_test.go") || filepath.Ext(path) != ".go" { + return nil } - return parser.parseFile(path) + + // positions are relative to FileSet + astFile, err := goparser.ParseFile(token.NewFileSet(), path, src, goparser.ParseComments) + if err != nil { + return fmt.Errorf("ParseFile error:%+v", err) + } + parser.packages.CollectAstFile(packageDir, path, astFile) + return nil } -func (parser *Parser) parseFile(path string) error { - if ext := filepath.Ext(path); ext == ".go" { - fset := token.NewFileSet() // positions are relative to fset - astFile, err := goparser.ParseFile(fset, path, nil, goparser.ParseComments) - if err != nil { - return fmt.Errorf("ParseFile error:%+v", err) +func (parser *Parser) checkOperationIDUniqueness() error { + // operationsIds contains all operationId annotations to check it's unique + operationsIds := make(map[string]string) + saveOperationID := func(operationID, currentPath string) error { + if operationID == "" { + return nil + } + if previousPath, ok := operationsIds[operationID]; ok { + return fmt.Errorf( + "duplicated @id annotation '%s' found in '%s', previously declared in: '%s'", + operationID, currentPath, previousPath) + } + operationsIds[operationID] = currentPath + return nil + } + getOperationID := func(itm spec.PathItem) (string, string) { + if itm.Get != nil { + return "GET", itm.Get.ID + } + if itm.Put != nil { + return "PUT", itm.Put.ID + } + if itm.Post != nil { + return "POST", itm.Post.ID + } + if itm.Delete != nil { + return "DELETE", itm.Delete.ID + } + if itm.Options != nil { + return "OPTIONS", itm.Options.ID + } + if itm.Head != nil { + return "HEAD", itm.Head.ID + } + if itm.Patch != nil { + return "PATCH", itm.Patch.ID + } + return "", "" + } + for path, itm := range parser.swagger.Paths.Paths { + method, id := getOperationID(itm) + if err := saveOperationID(id, fmt.Sprintf("%s %s", method, path)); err != nil { + return err } - - parser.files[path] = astFile } return nil } // Skip returns filepath.SkipDir error if match vendor and hidden folder func (parser *Parser) Skip(path string, f os.FileInfo) error { - - if !parser.ParseVendor { // ignore vendor - if f.IsDir() && f.Name() == "vendor" { + if f.IsDir() { + if !parser.ParseVendor && f.Name() == "vendor" || //ignore "vendor" + f.Name() == "docs" || //exclude docs + len(f.Name()) > 1 && f.Name()[0] == '.' { // exclude all hidden folder return filepath.SkipDir } - } - // issue - if f.IsDir() && f.Name() == "docs" { - return filepath.SkipDir + if parser.excludes != nil { + if _, ok := parser.excludes[path]; ok { + return filepath.SkipDir + } + } } - // exclude all hidden folder - if f.IsDir() && len(f.Name()) > 1 && f.Name()[0] == '.' { - return filepath.SkipDir - } return nil } @@ -1487,3 +1463,20 @@ func (parser *Parser) Skip(path string, f os.FileInfo) error { func (parser *Parser) GetSwagger() *spec.Swagger { return parser.swagger } + +//addTestType just for tests +func (parser *Parser) addTestType(typename string) { + if parser.parsedSchemas == nil { + parser.parsedSchemas = make(map[*TypeSpecDef]*Schema) + } + if parser.packages.uniqueDefinitions == nil { + parser.packages.uniqueDefinitions = make(map[string]*TypeSpecDef) + } + typeDef := &TypeSpecDef{} + parser.packages.uniqueDefinitions[typename] = typeDef + parser.parsedSchemas[typeDef] = &Schema{ + PkgPath: "", + Name: typename, + Schema: PrimitiveSchema(OBJECT), + } +} diff --git a/parser_test.go b/parser_test.go index 063f938af..6ab188d55 100644 --- a/parser_test.go +++ b/parser_test.go @@ -6,13 +6,14 @@ import ( "go/token" "io/ioutil" "os" - "path" "path/filepath" "testing" "github.com/stretchr/testify/assert" ) +const defaultParseDepth = 100 + func TestNew(t *testing.T) { swagMode = test New() @@ -38,7 +39,12 @@ func TestParser_ParseGeneralApiInfo(t *testing.T) { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "1.0" + "version": "1.0", + "x-logo": { + "altText": "Petstore logo", + "backgroundColor": "#FFFFFF", + "url": "https://redocly.github.io/redoc/petstore-logo.png" + } }, "host": "petstore.swagger.io", "basePath": "/v2", @@ -59,11 +65,13 @@ func TestParser_ParseGeneralApiInfo(t *testing.T) { "tokenUrl": "https://example.com/oauth/token", "scopes": { "admin": " Grants read and write access to administrative information" - } + }, + "x-tokenName": "id_token" }, "OAuth2Application": { "type": "oauth2", "flow": "application", + "authorizationUrl": "", "tokenUrl": "https://example.com/oauth/token", "scopes": { "admin": " Grants read and write access to administrative information", @@ -82,6 +90,7 @@ func TestParser_ParseGeneralApiInfo(t *testing.T) { "OAuth2Password": { "type": "oauth2", "flow": "password", + "authorizationUrl": "", "tokenUrl": "https://example.com/oauth/token", "scopes": { "admin": " Grants read and write access to administrative information", @@ -145,6 +154,7 @@ func TestParser_ParseGeneralApiInfoTemplated(t *testing.T) { "OAuth2Application": { "type": "oauth2", "flow": "application", + "authorizationUrl": "", "tokenUrl": "https://example.com/oauth/token", "scopes": { "admin": " Grants read and write access to administrative information", @@ -163,6 +173,7 @@ func TestParser_ParseGeneralApiInfoTemplated(t *testing.T) { "OAuth2Password": { "type": "oauth2", "flow": "password", + "authorizationUrl": "", "tokenUrl": "https://example.com/oauth/token", "scopes": { "admin": " Grants read and write access to administrative information", @@ -223,7 +234,6 @@ func TestParser_ParseGeneralApiInfoWithOpsInSameFile(t *testing.T) { "title": "Swagger Example API", "termsOfService": "http://swagger.io/terms/", "contact": {}, - "license": {}, "version": "1.0" }, "paths": {} @@ -250,28 +260,25 @@ func TestGetAllGoFileInfo(t *testing.T) { searchDir := "testdata/pet" p := New() - err := p.getAllGoFileInfo(searchDir) + err := p.getAllGoFileInfo("testdata", searchDir) assert.NoError(t, err) - assert.NotEmpty(t, p.files[filepath.Join("testdata", "pet", "main.go")]) - assert.NotEmpty(t, p.files[filepath.Join("testdata", "pet", "web", "handler.go")]) - assert.Equal(t, 2, len(p.files)) + assert.Equal(t, 2, len(p.packages.files)) } func TestParser_ParseType(t *testing.T) { searchDir := "testdata/simple/" p := New() - err := p.getAllGoFileInfo(searchDir) + err := p.getAllGoFileInfo("testdata", searchDir) assert.NoError(t, err) - for _, file := range p.files { - p.ParseType(file) - } + _, err = p.packages.ParseTypes() - assert.NotNil(t, p.TypeDefinitions["api"]["Pet3"]) - assert.NotNil(t, p.TypeDefinitions["web"]["Pet"]) - assert.NotNil(t, p.TypeDefinitions["web"]["Pet2"]) + 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 TestGetSchemes(t *testing.T) { @@ -281,6 +288,20 @@ func TestGetSchemes(t *testing.T) { } func TestParseSimpleApi1(t *testing.T) { + expected, err := ioutil.ReadFile("testdata/simple/expected.json") + assert.NoError(t, err) + searchDir := "testdata/simple" + mainAPIFile := "main.go" + p := New() + p.PropNamingStrategy = PascalCase + err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + + b, _ := json.MarshalIndent(p.swagger, "", " ") + assert.Equal(t, string(expected), string(b)) +} + +func TestParseSimpleApi_ForSnakecase(t *testing.T) { expected := `{ "swagger": "2.0", "info": { @@ -334,15 +355,6 @@ func TestParseSimpleApi1(t *testing.T) { "$ref": "#/definitions/web.APIError" } }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, "404": { "description": "Can not find ID", "schema": { @@ -516,75 +528,29 @@ func TestParseSimpleApi1(t *testing.T) { } }, "definitions": { - "api.SwagReturn": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "cross.Cross": { - "type": "object", - "properties": { - "Array": { - "type": "array", - "items": { - "type": "string" - } - }, - "String": { - "type": "string" - } - } - }, "web.APIError": { "type": "object", "properties": { - "CreatedAt": { + "created_at": { "type": "string" }, - "ErrorCode": { + "error_code": { "type": "integer" }, - "ErrorMessage": { + "error_message": { "type": "string" } } }, - "web.AnonymousStructArray": { - "type": "array", - "items": { - "type": "object", - "properties": { - "foo": { - "type": "string" - } - } - } - }, - "web.CrossAlias": { - "$ref": "#/definitions/cross.Cross" - }, - "web.IndirectRecursiveTest": { - "type": "object", - "properties": { - "Tags": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Tag" - } - } - } - }, "web.Pet": { "type": "object", "required": [ - "name", - "photo_urls" + "price" ], "properties": { + "birthday": { + "type": "integer" + }, "category": { "type": "object", "properties": { @@ -619,8 +585,6 @@ func TestParseSimpleApi1(t *testing.T) { }, "name": { "type": "string", - "maxLength": 16, - "minLength": 4, "example": "detail_category_name" }, "photo_urls": { @@ -637,50 +601,43 @@ func TestParseSimpleApi1(t *testing.T) { } } }, - "data": { - "type": "object" + "coeffs": { + "type": "array", + "items": { + "type": "number" + } }, - "decimal": { - "type": "number" + "custom_string": { + "type": "string" }, - "enum_array": { + "custom_string_arr": { "type": "array", "items": { - "type": "integer", - "enum": [ - 1, - 2, - 3, - 5, - 7 - ] + "type": "string" } }, + "data": { + "type": "object" + }, + "decimal": { + "type": "number" + }, "id": { "type": "integer", "format": "int64", - "readOnly": true, "example": 1 }, - "int_array": { - "type": "array", - "items": { - "type": "integer" - }, - "example": [ - 1, - 2 - ] - }, "is_alive": { "type": "boolean", - "default": true, "example": true }, "name": { "type": "string", "example": "poti" }, + "null_int": { + "type": "integer" + }, "pets": { "type": "array", "items": { @@ -705,16 +662,10 @@ func TestParseSimpleApi1(t *testing.T) { }, "price": { "type": "number", - "maximum": 1000, - "minimum": 1, "example": 3.25 }, "status": { - "type": "string", - "enum": [ - "healthy", - "ill" - ] + "type": "string" }, "tags": { "type": "array", @@ -736,75 +687,22 @@ func TestParseSimpleApi1(t *testing.T) { "id": { "type": "integer" }, - "middlename": { - "type": "string", - "x-abc": "def", - "x-nullable": true - } - } - }, - "web.Pet5a": { - "type": "object", - "required": [ - "name", - "odd" - ], - "properties": { - "name": { - "type": "string" - }, - "odd": { - "type": "boolean" - } - } - }, - "web.Pet5b": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - } - } - }, - "web.Pet5c": { - "type": "object", - "required": [ - "name", - "odd" - ], - "properties": { - "name": { + "middle_name": { "type": "string" - }, - "odd": { - "type": "boolean" } } }, "web.RevValue": { "type": "object", "properties": { - "Data": { + "data": { "type": "integer" }, - "Err": { + "err": { "type": "integer" }, - "Status": { + "status": { "type": "boolean" - }, - "cross": { - "type": "object", - "$ref": "#/definitions/cross.Cross" - }, - "crosses": { - "type": "array", - "items": { - "$ref": "#/definitions/cross.Cross" - } } } }, @@ -825,12 +723,6 @@ func TestParseSimpleApi1(t *testing.T) { } } } - }, - "web.Tags": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Tag" - } } }, "securityDefinitions": { @@ -854,6 +746,7 @@ func TestParseSimpleApi1(t *testing.T) { "OAuth2Application": { "type": "oauth2", "flow": "application", + "authorizationUrl": "", "tokenUrl": "https://example.com/oauth/token", "scopes": { "admin": " Grants read and write access to administrative information", @@ -872,6 +765,7 @@ func TestParseSimpleApi1(t *testing.T) { "OAuth2Password": { "type": "oauth2", "flow": "password", + "authorizationUrl": "", "tokenUrl": "https://example.com/oauth/token", "scopes": { "admin": " Grants read and write access to administrative information", @@ -881,18 +775,18 @@ func TestParseSimpleApi1(t *testing.T) { } } }` - searchDir := "testdata/simple" + searchDir := "testdata/simple2" mainAPIFile := "main.go" p := New() - p.PropNamingStrategy = PascalCase - err := p.ParseAPI(searchDir, mainAPIFile) + p.PropNamingStrategy = SnakeCase + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") assert.Equal(t, expected, string(b)) } -func TestParseSimpleApi_ForSnakecase(t *testing.T) { +func TestParseSimpleApi_ForLowerCamelcase(t *testing.T) { expected := `{ "swagger": "2.0", "info": { @@ -1122,26 +1016,20 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { "web.APIError": { "type": "object", "properties": { - "created_at": { + "createdAt": { "type": "string" }, - "error_code": { + "errorCode": { "type": "integer" }, - "error_message": { + "errorMessage": { "type": "string" } } }, "web.Pet": { "type": "object", - "required": [ - "price" - ], "properties": { - "birthday": { - "type": "integer" - }, "category": { "type": "object", "properties": { @@ -1153,7 +1041,7 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { "type": "string", "example": "category_name" }, - "photo_urls": { + "photoURLs": { "type": "array", "format": "url", "items": { @@ -1164,11 +1052,8 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { "http://test/image/2.jpg" ] }, - "small_category": { + "smallCategory": { "type": "object", - "required": [ - "name" - ], "properties": { "id": { "type": "integer", @@ -1178,7 +1063,7 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { "type": "string", "example": "detail_category_name" }, - "photo_urls": { + "photoURLs": { "type": "array", "items": { "type": "string" @@ -1192,21 +1077,6 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { } } }, - "coeffs": { - "type": "array", - "items": { - "type": "number" - } - }, - "custom_string": { - "type": "string" - }, - "custom_string_arr": { - "type": "array", - "items": { - "type": "string" - } - }, "data": { "type": "object" }, @@ -1218,7 +1088,7 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { "format": "int64", "example": 1 }, - "is_alive": { + "isAlive": { "type": "boolean", "example": true }, @@ -1226,9 +1096,6 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { "type": "string", "example": "poti" }, - "null_int": { - "type": "integer" - }, "pets": { "type": "array", "items": { @@ -1241,7 +1108,7 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { "$ref": "#/definitions/web.Pet2" } }, - "photo_urls": { + "photoURLs": { "type": "array", "items": { "type": "string" @@ -1272,13 +1139,13 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { "web.Pet2": { "type": "object", "properties": { - "deleted_at": { + "deletedAt": { "type": "string" }, "id": { "type": "integer" }, - "middle_name": { + "middleName": { "type": "string" } } @@ -1337,6 +1204,7 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { "OAuth2Application": { "type": "oauth2", "flow": "application", + "authorizationUrl": "", "tokenUrl": "https://example.com/oauth/token", "scopes": { "admin": " Grants read and write access to administrative information", @@ -1355,6 +1223,7 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { "OAuth2Password": { "type": "oauth2", "flow": "password", + "authorizationUrl": "", "tokenUrl": "https://example.com/oauth/token", "scopes": { "admin": " Grants read and write access to administrative information", @@ -1364,223 +1233,51 @@ func TestParseSimpleApi_ForSnakecase(t *testing.T) { } } }` - searchDir := "testdata/simple2" + searchDir := "testdata/simple3" mainAPIFile := "main.go" p := New() - p.PropNamingStrategy = SnakeCase - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") assert.Equal(t, expected, string(b)) } -func TestParseSimpleApi_ForLowerCamelcase(t *testing.T) { +func TestParseStructComment(t *testing.T) { expected := `{ "swagger": "2.0", "info": { "description": "This is a sample server Petstore server.", "title": "Swagger Example API", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "name": "API Support", - "url": "http://www.swagger.io/support", - "email": "support@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, + "contact": {}, "version": "1.0" }, - "host": "petstore.swagger.io", - "basePath": "/v2", + "host": "localhost:4000", + "basePath": "/api", "paths": { - "/file/upload": { - "post": { - "description": "Upload file", + "/posts/{post_id}": { + "get": { + "description": "get string by ID", "consumes": [ - "multipart/form-data" + "application/json" ], "produces": [ "application/json" ], - "summary": "Upload file", - "operationId": "file.upload", + "summary": "Add a new pet to the store", "parameters": [ { - "type": "file", - "description": "this is a test file", - "name": "file", - "in": "formData", + "type": "integer", + "format": "int64", + "description": "Some ID", + "name": "post_id", + "in": "path", "required": true } ], "responses": { "200": { - "description": "ok", - "schema": { - "type": "string" - } - }, - "400": { - "description": "We need ID!!", - "schema": { - "$ref": "#/definitions/web.APIError" - } - }, - "404": { - "description": "Can not find ID", - "schema": { - "$ref": "#/definitions/web.APIError" - } - } - } - } - }, - "/testapi/get-string-by-int/{some_id}": { - "get": { - "description": "get string by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Add a new pet to the store", - "operationId": "get-string-by-int", - "parameters": [ - { - "type": "integer", - "format": "int64", - "description": "Some ID", - "name": "some_id", - "in": "path", - "required": true - }, - { - "description": "Some ID", - "name": "some_id", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/web.Pet" - } - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "type": "string" - } - }, - "400": { - "description": "We need ID!!", - "schema": { - "$ref": "#/definitions/web.APIError" - } - }, - "404": { - "description": "Can not find ID", - "schema": { - "$ref": "#/definitions/web.APIError" - } - } - } - } - }, - "/testapi/get-struct-array-by-string/{some_id}": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - }, - { - "BasicAuth": [] - }, - { - "OAuth2Application": [ - "write" - ] - }, - { - "OAuth2Implicit": [ - "read", - "admin" - ] - }, - { - "OAuth2AccessCode": [ - "read" - ] - }, - { - "OAuth2Password": [ - "admin" - ] - } - ], - "description": "get struct array by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "operationId": "get-struct-array-by-string", - "parameters": [ - { - "type": "string", - "description": "Some ID", - "name": "some_id", - "in": "path", - "required": true - }, - { - "enum": [ - 1, - 2, - 3 - ], - "type": "integer", - "description": "Category", - "name": "category", - "in": "query", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "default": 0, - "description": "Offset", - "name": "offset", - "in": "query", - "required": true - }, - { - "maximum": 50, - "type": "integer", - "default": 10, - "description": "Limit", - "name": "limit", - "in": "query", - "required": true - }, - { - "maxLength": 50, - "minLength": 1, - "type": "string", - "default": "\"\"", - "description": "q", - "name": "q", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "ok", + "description": "OK", "schema": { "type": "string" } @@ -1606,280 +1303,61 @@ func TestParseSimpleApi_ForLowerCamelcase(t *testing.T) { "type": "object", "properties": { "createdAt": { - "type": "string" - }, - "errorCode": { - "type": "integer" - }, - "errorMessage": { - "type": "string" - } - } - }, - "web.Pet": { - "type": "object", - "properties": { - "category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "category_name" - }, - "photoURLs": { - "type": "array", - "format": "url", - "items": { - "type": "string" - }, - "example": [ - "http://test/image/1.jpg", - "http://test/image/2.jpg" - ] - }, - "smallCategory": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "detail_category_name" - }, - "photoURLs": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "http://test/image/1.jpg", - "http://test/image/2.jpg" - ] - } - } - } - } - }, - "data": { - "type": "object" - }, - "decimal": { - "type": "number" - }, - "id": { - "type": "integer", - "format": "int64", - "example": 1 - }, - "isAlive": { - "type": "boolean", - "example": true - }, - "name": { - "type": "string", - "example": "poti" - }, - "pets": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Pet2" - } - }, - "pets2": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Pet2" - } - }, - "photoURLs": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "http://test/image/1.jpg", - "http://test/image/2.jpg" - ] - }, - "price": { - "type": "number", - "example": 3.25 - }, - "status": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Tag" - } - }, - "uuid": { - "type": "string" - } - } - }, - "web.Pet2": { - "type": "object", - "properties": { - "deletedAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "middleName": { - "type": "string" - } - } - }, - "web.RevValue": { - "type": "object", - "properties": { - "data": { - "type": "integer" - }, - "err": { - "type": "integer" - }, - "status": { - "type": "boolean" - } - } - }, - "web.Tag": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" + "description": "Error time", + "type": "string" }, - "name": { + "error": { + "description": "Error an Api error", "type": "string" }, - "pets": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Pet" - } + "errorCtx": { + "description": "Error ` + "`" + `context` + "`" + ` tick comment", + "type": "string" + }, + "errorNo": { + "description": "Error ` + "`" + `number` + "`" + ` tick comment", + "type": "integer" } } } - }, - "securityDefinitions": { - "ApiKeyAuth": { - "type": "apiKey", - "name": "Authorization", - "in": "header" - }, - "BasicAuth": { - "type": "basic" - }, - "OAuth2AccessCode": { - "type": "oauth2", - "flow": "accessCode", - "authorizationUrl": "https://example.com/oauth/authorize", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information" - } - }, - "OAuth2Application": { - "type": "oauth2", - "flow": "application", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information", - "write": " Grants write access" - } - }, - "OAuth2Implicit": { - "type": "oauth2", - "flow": "implicit", - "authorizationUrl": "https://example.com/oauth/authorize", - "scopes": { - "admin": " Grants read and write access to administrative information", - "write": " Grants write access" - } - }, - "OAuth2Password": { - "type": "oauth2", - "flow": "password", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information", - "read": " Grants read access", - "write": " Grants write access" - } - } } }` - searchDir := "testdata/simple3" + searchDir := "testdata/struct_comment" mainAPIFile := "main.go" p := New() - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) - b, _ := json.MarshalIndent(p.swagger, "", " ") assert.Equal(t, expected, string(b)) } -func TestParseStructComment(t *testing.T) { +func TestParseNonExportedJSONFields(t *testing.T) { expected := `{ "swagger": "2.0", "info": { - "description": "This is a sample server Petstore server.", + "description": "This is a sample server.", "title": "Swagger Example API", "contact": {}, - "license": {}, "version": "1.0" }, "host": "localhost:4000", "basePath": "/api", "paths": { - "/posts/{post_id}": { + "/so-something": { "get": { - "description": "get string by ID", + "description": "Does something, but internal (non-exported) fields inside a struct won't be marshaled into JSON", "consumes": [ "application/json" ], "produces": [ "application/json" ], - "summary": "Add a new pet to the store", - "parameters": [ - { - "type": "integer", - "format": "int64", - "description": "Some ID", - "name": "post_id", - "in": "path", - "required": true - } - ], + "summary": "Call DoSomething", "responses": { "200": { "description": "OK", "schema": { - "type": "string" - } - }, - "400": { - "description": "We need ID!!", - "schema": { - "$ref": "#/definitions/web.APIError" - } - }, - "404": { - "description": "Can not find ID", - "schema": { - "$ref": "#/definitions/web.APIError" + "$ref": "#/definitions/main.MyStruct" } } } @@ -1887,28 +1365,7 @@ func TestParseStructComment(t *testing.T) { } }, "definitions": { - "web.APIError": { - "type": "object", - "properties": { - "createdAt": { - "description": "Error time", - "type": "string" - }, - "error": { - "description": "Error an Api error", - "type": "string" - }, - "errorCtx": { - "description": "Error ` + "`" + `context` + "`" + ` tick comment", - "type": "string" - }, - "errorNo": { - "description": "Error ` + "`" + `number` + "`" + ` tick comment", - "type": "integer" - } - } - }, - "web.Post": { + "main.MyStruct": { "type": "object", "properties": { "data": { @@ -1938,10 +1395,11 @@ func TestParseStructComment(t *testing.T) { } } }` - searchDir := "testdata/struct_comment" + + searchDir := "testdata/non_exported_json_fields" mainAPIFile := "main.go" p := New() - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") assert.Equal(t, expected, string(b)) @@ -1965,168 +1423,16 @@ func TestParsePetApi(t *testing.T) { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "1.0" - }, - "host": "petstore.swagger.io", - "basePath": "/v2", - "paths": {} -}` - searchDir := "testdata/pet" - mainAPIFile := "main.go" - p := New() - err := p.ParseAPI(searchDir, mainAPIFile) - assert.NoError(t, err) - b, _ := json.MarshalIndent(p.swagger, "", " ") - assert.Equal(t, expected, string(b)) -} - -func TestParseModelNotUnderRoot(t *testing.T) { - expected := `{ - "swagger": "2.0", - "info": { - "description": "This is a sample server Petstore server.", - "title": "Swagger Example API", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "name": "API Support", - "url": "http://www.swagger.io/support", - "email": "support@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0" - }, - "host": "petstore.swagger.io", - "basePath": "/v2", - "paths": { - "/file/upload": { - "post": { - "description": "Upload file", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Upload file", - "operationId": "file.upload", - "parameters": [ - { - "description": "Foo to create", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/data.Foo" - } - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "type": "string" - } - } - } - } - }, - "/testapi/get-string-by-int/{some_id}": { - "get": { - "description": "get string by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Add a new pet to the store", - "operationId": "get-string-by-int", - "parameters": [ - { - "type": "integer", - "format": "int64", - "description": "Some ID", - "name": "some_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/data.Foo" - } - } - } - } - } - }, - "definitions": { - "data.Foo": { - "type": "object", - "properties": { - "field1": { - "type": "string" - } - } - } - }, - "securityDefinitions": { - "ApiKeyAuth": { - "type": "apiKey", - "name": "Authorization", - "in": "header" - }, - "BasicAuth": { - "type": "basic" - }, - "OAuth2AccessCode": { - "type": "oauth2", - "flow": "accessCode", - "authorizationUrl": "https://example.com/oauth/authorize", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information" - } - }, - "OAuth2Application": { - "type": "oauth2", - "flow": "application", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information", - "write": " Grants write access" - } - }, - "OAuth2Implicit": { - "type": "oauth2", - "flow": "implicit", - "authorizationUrl": "https://example.com/oauth/authorize", - "scopes": { - "admin": " Grants read and write access to administrative information", - "write": " Grants write access" - } - }, - "OAuth2Password": { - "type": "oauth2", - "flow": "password", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information", - "read": " Grants read access", - "write": " Grants write access" - } - } - } + "version": "1.0" + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "paths": {} }` - searchDir := "testdata/model_not_under_root/cmd" + searchDir := "testdata/pet" mainAPIFile := "main.go" p := New() - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") assert.Equal(t, expected, string(b)) @@ -2195,7 +1501,7 @@ func TestParseModelAsTypeAlias(t *testing.T) { searchDir := "testdata/alias_type" mainAPIFile := "main.go" p := New() - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") @@ -2206,10 +1512,10 @@ func TestParseComposition(t *testing.T) { searchDir := "testdata/composition" mainAPIFile := "main.go" p := New() - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) - expected, err := ioutil.ReadFile(path.Join(searchDir, "expected.json")) + expected, err := ioutil.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") @@ -2222,10 +1528,10 @@ func TestParseImportAliases(t *testing.T) { searchDir := "testdata/alias_import" mainAPIFile := "main.go" p := New() - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) - expected, err := ioutil.ReadFile(path.Join(searchDir, "expected.json")) + expected, err := ioutil.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") @@ -2238,16 +1544,47 @@ func TestParseNested(t *testing.T) { mainAPIFile := "main.go" p := New() p.ParseDependency = true - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) - expected, err := ioutil.ReadFile(path.Join(searchDir, "expected.json")) + expected, err := ioutil.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") assert.Equal(t, string(expected), string(b)) } +func TestParseDuplicated(t *testing.T) { + searchDir := "testdata/duplicated" + mainAPIFile := "main.go" + p := New() + p.ParseDependency = true + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.Errorf(t, err, "duplicated @id declarations successfully found") +} + +func TestParseDuplicatedOtherMethods(t *testing.T) { + searchDir := "testdata/duplicated2" + mainAPIFile := "main.go" + p := New() + p.ParseDependency = true + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.Errorf(t, err, "duplicated @id declarations successfully found") +} + +func TestParseConflictSchemaName(t *testing.T) { + searchDir := "testdata/conflict_name" + mainAPIFile := "main.go" + p := New() + p.ParseDependency = true + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + b, _ := json.MarshalIndent(p.swagger, "", " ") + expected, err := ioutil.ReadFile(filepath.Join(searchDir, "expected.json")) + assert.NoError(t, err) + assert.Equal(t, string(expected), string(b)) +} + func TestParser_ParseStructArrayObject(t *testing.T) { src := ` package api @@ -2256,9 +1593,9 @@ type Response struct { Code int Table [][]string Data []struct{ - Field1 uint - Field2 string - } + Field1 uint + Field2 string + } } // @Success 200 {object} Response @@ -2303,12 +1640,11 @@ func Test(){ assert.NoError(t, err) p := New() - p.ParseType(f) - err = p.ParseRouterAPIInfo("", f) + p.packages.CollectAstFile("api", "api/api.go", f) + _, err = p.packages.ParseTypes() assert.NoError(t, err) - typeSpec := p.TypeDefinitions["api"]["Response"] - err = p.ParseDefinition("api", typeSpec.Name.Name, typeSpec) + err = p.ParseRouterAPIInfo("", f) assert.NoError(t, err) out, err := json.MarshalIndent(p.swagger.Definitions, "", " ") @@ -2334,7 +1670,7 @@ func Test(){ package rest type ResponseWrapper struct { - Status string + Status string Code int Messages []string Result interface{} @@ -2367,17 +1703,16 @@ type ResponseWrapper struct { f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments) assert.NoError(t, err) - parser.ParseType(f) + parser.packages.CollectAstFile("api", "api/api.go", f) f2, err := goparser.ParseFile(token.NewFileSet(), "", restsrc, goparser.ParseComments) assert.NoError(t, err) - parser.ParseType(f2) + parser.packages.CollectAstFile("rest", "rest/rest.go", f2) - err = parser.ParseRouterAPIInfo("", f) + _, err = parser.packages.ParseTypes() assert.NoError(t, err) - typeSpec := parser.TypeDefinitions["api"]["Response"] - err = parser.ParseDefinition("api", typeSpec.Name.Name, typeSpec) + err = parser.ParseRouterAPIInfo("", f) assert.NoError(t, err) out, err := json.MarshalIndent(parser.swagger.Definitions, "", " ") @@ -2423,7 +1758,6 @@ func Test(){ }, "test2": { "description": "test2", - "type": "object", "$ref": "#/definitions/api.Child" } } @@ -2434,12 +1768,11 @@ func Test(){ assert.NoError(t, err) p := New() - p.ParseType(f) - err = p.ParseRouterAPIInfo("", f) + p.packages.CollectAstFile("api", "api/api.go", f) + _, err = p.packages.ParseTypes() assert.NoError(t, err) - typeSpec := p.TypeDefinitions["api"]["Parent"] - err = p.ParseDefinition("api", typeSpec.Name.Name, typeSpec) + err = p.ParseRouterAPIInfo("", f) assert.NoError(t, err) out, err := json.MarshalIndent(p.swagger.Definitions, "", " ") @@ -2527,7 +1860,6 @@ func Test(){ }, "test6": { "description": "test6", - "type": "object", "$ref": "#/definitions/api.MyMapType" }, "test7": { @@ -2561,12 +1893,12 @@ func Test(){ assert.NoError(t, err) p := New() - p.ParseType(f) - err = p.ParseRouterAPIInfo("", f) + p.packages.CollectAstFile("api", "api/api.go", f) + + _, err = p.packages.ParseTypes() assert.NoError(t, err) - typeSpec := p.TypeDefinitions["api"]["Parent"] - err = p.ParseDefinition("api", typeSpec.Name.Name, typeSpec) + err = p.ParseRouterAPIInfo("", f) assert.NoError(t, err) out, err := json.MarshalIndent(p.swagger.Definitions, "", " ") @@ -2784,64 +2116,6 @@ func Test3(){ assert.NotNil(t, val.Delete) } -func TestSkip(t *testing.T) { - folder1 := "/tmp/vendor" - err := os.Mkdir(folder1, os.ModePerm) - assert.NoError(t, err) - f1, _ := os.Stat(folder1) - - parser := New() - - assert.True(t, parser.Skip(folder1, f1) == filepath.SkipDir) - assert.NoError(t, os.Remove(folder1)) - - folder2 := "/tmp/.git" - err = os.Mkdir(folder2, os.ModePerm) - assert.NoError(t, err) - f2, _ := os.Stat(folder2) - - assert.True(t, parser.Skip(folder2, f2) == filepath.SkipDir) - assert.NoError(t, os.Remove(folder2)) - - currentPath := "./" - currentPathInfo, _ := os.Stat(currentPath) - assert.True(t, parser.Skip(currentPath, currentPathInfo) == nil) -} - -func TestSkipMustParseVendor(t *testing.T) { - folder1 := "/tmp/vendor" - err := os.Mkdir(folder1, os.ModePerm) - assert.NoError(t, err) - - f1, _ := os.Stat(folder1) - - parser := New() - parser.ParseVendor = true - - assert.True(t, parser.Skip(folder1, f1) == nil) - assert.NoError(t, os.Remove(folder1)) - - folder2 := "/tmp/.git" - err = os.Mkdir(folder2, os.ModePerm) - assert.NoError(t, err) - - f2, _ := os.Stat(folder2) - - assert.True(t, parser.Skip(folder2, f2) == filepath.SkipDir) - assert.NoError(t, os.Remove(folder2)) - - currentPath := "./" - currentPathInfo, _ := os.Stat(currentPath) - assert.True(t, parser.Skip(currentPath, currentPathInfo) == nil) - - folder3 := "/tmp/test/vendor/github.com/swaggo/swag" - assert.NoError(t, os.MkdirAll(folder3, os.ModePerm)) - f3, _ := os.Stat(folder3) - - assert.Nil(t, parser.Skip(folder3, f3)) - assert.NoError(t, os.RemoveAll("/tmp/test")) -} - // func TestParseDeterministic(t *testing.T) { // mainAPIFile := "main.go" // for _, searchDir := range []string{ @@ -2855,7 +2129,7 @@ func TestSkipMustParseVendor(t *testing.T) { // for i := 0; i < 100; i++ { // p := New() // p.PropNamingStrategy = PascalCase -// err := p.ParseAPI(searchDir, mainAPIFile) +// err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) // b, _ := json.MarshalIndent(p.swagger, "", " ") // assert.NotEqual(t, "", string(b)) @@ -2874,7 +2148,7 @@ func TestApiParseTag(t *testing.T) { mainAPIFile := "main.go" p := New(SetMarkdownFileDirectory(searchDir)) p.PropNamingStrategy = PascalCase - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) if len(p.swagger.Tags) != 3 { @@ -2898,12 +2172,21 @@ func TestApiParseTag(t *testing.T) { } } +func TestApiParseTag_NonExistendTag(t *testing.T) { + searchDir := "testdata/tags_nonexistend_tag" + mainAPIFile := "main.go" + p := New(SetMarkdownFileDirectory(searchDir)) + p.PropNamingStrategy = PascalCase + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.Error(t, err) +} + func TestParseTagMarkdownDescription(t *testing.T) { searchDir := "testdata/tags" mainAPIFile := "main.go" p := New(SetMarkdownFileDirectory(searchDir)) p.PropNamingStrategy = PascalCase - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) if err != nil { t.Error("Failed to parse api description: " + err.Error()) } @@ -2923,7 +2206,7 @@ func TestParseApiMarkdownDescription(t *testing.T) { mainAPIFile := "main.go" p := New(SetMarkdownFileDirectory(searchDir)) p.PropNamingStrategy = PascalCase - err := p.ParseAPI(searchDir, mainAPIFile) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) if err != nil { t.Error("Failed to parse api description: " + err.Error()) } @@ -2937,7 +2220,7 @@ func TestIgnoreInvalidPkg(t *testing.T) { searchDir := "testdata/deps_having_invalid_pkg" mainAPIFile := "main.go" p := New() - if err := p.ParseAPI(searchDir, mainAPIFile); err != nil { + if err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth); err != nil { t.Error("Failed to ignore valid pkg: " + err.Error()) } } @@ -2947,7 +2230,7 @@ func TestFixes432(t *testing.T) { mainAPIFile := "cmd/main.go" p := New() - if err := p.ParseAPI(searchDir, mainAPIFile); err != nil { + if err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth); err != nil { t.Error("Failed to ignore valid pkg: " + err.Error()) } } @@ -2958,7 +2241,7 @@ func TestParseOutsideDependencies(t *testing.T) { p := New() p.ParseDependency = true - if err := p.ParseAPI(searchDir, mainAPIFile); err != nil { + if err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth); err != nil { t.Error("Failed to parse api: " + err.Error()) } } @@ -2979,15 +2262,296 @@ type Student struct { // @Router /test [get] func Fun() { +} +` + expected := `{ + "info": { + "contact": {} + }, + "paths": { + "/test": { + "get": { + "parameters": [ + { + "type": "integer", + "name": "age", + "in": "query" + }, + { + "type": "string", + "name": "name", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "name": "teachers", + "in": "query" + } + ], + "responses": { + "200": { + "description": "" + } + } + } + } + } +}` + + f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments) + assert.NoError(t, err) + + p := New() + p.packages.CollectAstFile("api", "api/api.go", f) + + _, err = p.packages.ParseTypes() + assert.NoError(t, err) + + err = p.ParseRouterAPIInfo("", f) + assert.NoError(t, err) + + b, _ := json.MarshalIndent(p.swagger, "", " ") + assert.Equal(t, expected, string(b)) +} + +func TestParseRenamedStructDefinition(t *testing.T) { + src := ` +package main + +type Child struct { + Name string +}//@name Student + +type Parent struct { + Name string + Child Child +}//@name Teacher + +// @Param request body Parent true "query params" +// @Success 200 {object} Parent +// @Router /test [get] +func Fun() { + +} +` + f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments) + assert.NoError(t, err) + + p := New() + p.packages.CollectAstFile("api", "api/api.go", f) + _, err = p.packages.ParseTypes() + assert.NoError(t, err) + + err = p.ParseRouterAPIInfo("", f) + assert.NoError(t, err) + + assert.NoError(t, err) + teacher, ok := p.swagger.Definitions["Teacher"] + assert.True(t, ok) + ref := teacher.Properties["child"].SchemaProps.Ref + assert.Equal(t, "#/definitions/Student", ref.String()) + _, ok = p.swagger.Definitions["Student"] + assert.True(t, ok) + path, ok := p.swagger.Paths.Paths["/test"] + assert.True(t, ok) + assert.Equal(t, "#/definitions/Teacher", path.Get.Parameters[0].Schema.Ref.String()) + ref = path.Get.Responses.ResponsesProps.StatusCodeResponses[200].ResponseProps.Schema.Ref + assert.Equal(t, "#/definitions/Teacher", ref.String()) +} + +func TestParseJSONFieldString(t *testing.T) { + expected := `{ + "swagger": "2.0", + "info": { + "description": "This is a sample server.", + "title": "Swagger Example API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:4000", + "basePath": "/", + "paths": { + "/do-something": { + "post": { + "description": "Does something", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Call DoSomething", + "parameters": [ + { + "description": "My Struct", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.MyStruct" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.MyStruct" + } + }, + "500": { + "description": "" + } + } + } + } + }, + "definitions": { + "main.MyStruct": { + "type": "object", + "properties": { + "boolvar": { + "description": "boolean as a string", + "type": "string", + "example": "false" + }, + "floatvar": { + "description": "float as a string", + "type": "string", + "example": "0" + }, + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "myint": { + "description": "integer as string", + "type": "string", + "example": "0" + }, + "name": { + "type": "string", + "example": "poti" + }, + "truebool": { + "description": "boolean as a string", + "type": "string", + "example": "true" + } + } + } + } +}` + + searchDir := "testdata/json_field_string" + mainAPIFile := "main.go" + p := New() + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + b, _ := json.MarshalIndent(p.swagger, "", " ") + assert.Equal(t, expected, string(b)) +} + +func TestParseSwaggerignoreForEmbedded(t *testing.T) { + src := ` +package main + +type Child struct { + ChildName string +}//@name Student + +type Parent struct { + Name string + Child ` + "`swaggerignore:\"true\"`" + ` +}//@name Teacher + +// @Param request body Parent true "query params" +// @Success 200 {object} Parent +// @Router /test [get] +func Fun() { + } ` f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments) assert.NoError(t, err) p := New() - p.ParseType(f) + p.packages.CollectAstFile("api", "api/api.go", f) + p.packages.ParseTypes() err = p.ParseRouterAPIInfo("", f) assert.NoError(t, err) - assert.Equal(t, 3, len(p.swagger.Paths.Paths["/test"].Get.Parameters)) + assert.NoError(t, err) + teacher, ok := p.swagger.Definitions["Teacher"] + assert.True(t, ok) + + name, ok := teacher.Properties["name"] + assert.True(t, ok) + assert.Len(t, name.Type, 1) + assert.Equal(t, "string", name.Type[0]) + + childName, ok := teacher.Properties["childName"] + assert.False(t, ok) + assert.Empty(t, childName) +} + +func TestDefineTypeOfExample(t *testing.T) { + var example interface{} + var err error + + example, err = defineTypeOfExample("string", "", "example") + assert.NoError(t, err) + assert.Equal(t, example.(string), "example") + + example, err = defineTypeOfExample("number", "", "12.34") + assert.NoError(t, err) + assert.Equal(t, example.(float64), 12.34) + + example, err = defineTypeOfExample("boolean", "", "true") + assert.NoError(t, err) + assert.Equal(t, example.(bool), true) + + example, err = defineTypeOfExample("array", "", "one,two,three") + assert.Error(t, err) + assert.Nil(t, example) + + example, err = defineTypeOfExample("array", "string", "one,two,three") + assert.NoError(t, err) + arr := []string{} + + for _, v := range example.([]interface{}) { + arr = append(arr, v.(string)) + } + + assert.Equal(t, arr, []string{"one", "two", "three"}) + + example, err = defineTypeOfExample("object", "", "key_one:one,key_two:two,key_three:three") + assert.Error(t, err) + assert.Nil(t, example) + + example, err = defineTypeOfExample("object", "string", "key_one,key_two,key_three") + assert.Error(t, err) + assert.Nil(t, example) + + example, err = defineTypeOfExample("object", "oops", "key_one:one,key_two:two,key_three:three") + assert.Error(t, err) + assert.Nil(t, example) + + example, err = defineTypeOfExample("object", "string", "key_one:one,key_two:two,key_three:three") + assert.NoError(t, err) + obj := map[string]string{} + + for k, v := range example.(map[string]interface{}) { + obj[k] = v.(string) + } + + assert.Equal(t, obj, map[string]string{"key_one": "one", "key_two": "two", "key_three": "three"}) + + example, err = defineTypeOfExample("oops", "", "") + assert.Error(t, err) + assert.Nil(t, example) } diff --git a/property.go b/property.go deleted file mode 100644 index 3a3fbbcbc..000000000 --- a/property.go +++ /dev/null @@ -1,139 +0,0 @@ -package swag - -import ( - "errors" - "fmt" - "go/ast" - "strings" -) - -// ErrFailedConvertPrimitiveType Failed to convert for swag to interpretable type -var ErrFailedConvertPrimitiveType = errors.New("swag property: failed convert primitive type") - -type propertyName struct { - SchemaType string - ArrayType string - CrossPkg string -} - -type propertyNewFunc func(schemeType string, crossPkg string) propertyName - -func newArrayProperty(schemeType string, crossPkg string) propertyName { - return propertyName{ - SchemaType: "array", - ArrayType: schemeType, - CrossPkg: crossPkg, - } -} - -func newProperty(schemeType string, crossPkg string) propertyName { - return propertyName{ - SchemaType: schemeType, - ArrayType: "string", - CrossPkg: crossPkg, - } -} - -func convertFromSpecificToPrimitive(typeName string) (string, error) { - typeName = strings.ToUpper(typeName) - switch typeName { - case "TIME", "OBJECTID", "UUID": - return "string", nil - case "DECIMAL": - return "number", nil - } - return "", ErrFailedConvertPrimitiveType -} - -func parseFieldSelectorExpr(astTypeSelectorExpr *ast.SelectorExpr, parser *Parser, propertyNewFunc propertyNewFunc) propertyName { - if primitiveType, err := convertFromSpecificToPrimitive(astTypeSelectorExpr.Sel.Name); err == nil { - return propertyNewFunc(primitiveType, "") - } - - if pkgName, ok := astTypeSelectorExpr.X.(*ast.Ident); ok { - if typeDefinitions, ok := parser.TypeDefinitions[pkgName.Name][astTypeSelectorExpr.Sel.Name]; ok { - if expr, ok := typeDefinitions.Type.(*ast.SelectorExpr); ok { - if primitiveType, err := convertFromSpecificToPrimitive(expr.Sel.Name); err == nil { - return propertyNewFunc(primitiveType, "") - } - } - parser.ParseDefinition(pkgName.Name, astTypeSelectorExpr.Sel.Name, typeDefinitions) - return propertyNewFunc(astTypeSelectorExpr.Sel.Name, pkgName.Name) - } - if aliasedNames, ok := parser.ImportAliases[pkgName.Name]; ok { - for aliasedName := range aliasedNames { - if typeDefinitions, ok := parser.TypeDefinitions[aliasedName][astTypeSelectorExpr.Sel.Name]; ok { - if expr, ok := typeDefinitions.Type.(*ast.SelectorExpr); ok { - if primitiveType, err := convertFromSpecificToPrimitive(expr.Sel.Name); err == nil { - return propertyNewFunc(primitiveType, "") - } - } - parser.ParseDefinition(aliasedName, astTypeSelectorExpr.Sel.Name, typeDefinitions) - return propertyNewFunc(astTypeSelectorExpr.Sel.Name, aliasedName) - } - } - } - name := fmt.Sprintf("%s.%v", pkgName, astTypeSelectorExpr.Sel.Name) - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[name]; isCustomType { - return propertyName{SchemaType: actualPrimitiveType, ArrayType: actualPrimitiveType} - } - } - return propertyName{SchemaType: "string", ArrayType: "string"} -} - -// getPropertyName returns the string value for the given field if it exists -// allowedValues: array, boolean, integer, null, number, object, string -func getPropertyName(pkgName string, expr ast.Expr, parser *Parser) (propertyName, error) { - switch tp := expr.(type) { - case *ast.SelectorExpr: - return parseFieldSelectorExpr(tp, parser, newProperty), nil - case *ast.StarExpr: - return getPropertyName(pkgName, tp.X, parser) - case *ast.ArrayType: - return getArrayPropertyName(pkgName, tp.Elt, parser), nil - case *ast.MapType, *ast.StructType, *ast.InterfaceType: - return propertyName{SchemaType: "object", ArrayType: "object"}, nil - case *ast.FuncType: - return propertyName{SchemaType: "func", ArrayType: ""}, nil - case *ast.Ident: - name := tp.Name - // check if it is a custom type - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[pkgName+"."+name]; isCustomType { - return propertyName{SchemaType: actualPrimitiveType, ArrayType: actualPrimitiveType}, nil - } - - name = TransToValidSchemeType(name) - return propertyName{SchemaType: name, ArrayType: name}, nil - default: - return propertyName{}, errors.New("not supported" + fmt.Sprint(expr)) - } -} - -func getArrayPropertyName(pkgName string, astTypeArrayElt ast.Expr, parser *Parser) propertyName { - switch elt := astTypeArrayElt.(type) { - case *ast.StructType, *ast.MapType, *ast.InterfaceType: - return propertyName{SchemaType: "array", ArrayType: "object"} - case *ast.ArrayType: - return propertyName{SchemaType: "array", ArrayType: "array"} - case *ast.StarExpr: - return getArrayPropertyName(pkgName, elt.X, parser) - case *ast.SelectorExpr: - return parseFieldSelectorExpr(elt, parser, newArrayProperty) - case *ast.Ident: - name := elt.Name - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[pkgName+"."+name]; isCustomType { - name = actualPrimitiveType - } else { - name = TransToValidSchemeType(elt.Name) - } - return propertyName{SchemaType: "array", ArrayType: name} - default: - name := fmt.Sprintf("%s", astTypeArrayElt) - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[pkgName+"."+name]; isCustomType { - name = actualPrimitiveType - } else { - name = TransToValidSchemeType(name) - } - return propertyName{SchemaType: "array", ArrayType: name} - } -} diff --git a/property_test.go b/property_test.go deleted file mode 100644 index 2e65183b6..000000000 --- a/property_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package swag - -import ( - "go/ast" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetPropertyNameSelectorExpr(t *testing.T) { - input := &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "time", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "Time", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - "string", - "string", - "", - } - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameIdentObjectId(t *testing.T) { - input := &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "hoge", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "ObjectId", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - "string", - "string", - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameIdentUUID(t *testing.T) { - input := &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "hoge", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "uuid", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - "string", - "string", - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameIdentDecimal(t *testing.T) { - input := &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "hoge", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "Decimal", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - "number", - "string", - "", - } - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameIdentTime(t *testing.T) { - input := &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "hoge", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "Time", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - "string", - "string", - "", - } - - propertyName, err := getPropertyName("test", input, nil) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameStarExprIdent(t *testing.T) { - input := &ast.StarExpr{ - Star: 1026, - X: &ast.Ident{ - NamePos: 1027, - Name: "string", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - "string", - "string", - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameStarExprMap(t *testing.T) { - input := &ast.StarExpr{ - Star: 1026, - X: &ast.MapType{ - Map: 1027, - Key: &ast.Ident{ - NamePos: 1034, - Name: "string", - Obj: (*ast.Object)(nil), - }, - Value: &ast.Ident{ - NamePos: 1041, - Name: "string", - Obj: (*ast.Object)(nil), - }, - }, - } - expected := propertyName{ - "object", - "object", - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameArrayStarExpr(t *testing.T) { - input := &ast.ArrayType{ - Lbrack: 465, - Len: nil, - Elt: &ast.StarExpr{ - X: &ast.Ident{ - NamePos: 467, - Name: "string", - Obj: (*ast.Object)(nil), - }, - }, - } - expected := propertyName{ - "array", - "string", - "", - } - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameArrayStarExprSelector(t *testing.T) { - input := &ast.ArrayType{ - Lbrack: 1111, - Len: nil, - Elt: &ast.StarExpr{ - X: &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "hoge", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "ObjectId", - Obj: (*ast.Object)(nil), - }, - }, - }, - } - expected := propertyName{ - "array", - "string", - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameArrayStructType(t *testing.T) { - input := &ast.ArrayType{ - Lbrack: 1111, - Len: nil, - Elt: &ast.StructType{}, - } - expected := propertyName{ - "array", - "object", - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameMap(t *testing.T) { - input := &ast.MapType{ - Key: &ast.Ident{ - Name: "string", - }, - Value: &ast.Ident{ - Name: "string", - }, - } - expected := propertyName{ - "object", - "object", - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameStruct(t *testing.T) { - input := &ast.StructType{} - expected := propertyName{ - "object", - "object", - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameInterface(t *testing.T) { - input := &ast.InterfaceType{} - expected := propertyName{ - "object", - "object", - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameChannel(t *testing.T) { - input := &ast.ChanType{} - _, err := getPropertyName("test", input, New()) - assert.Error(t, err) -} - -func TestParseTag(t *testing.T) { - searchDir := "testdata/tags" - mainAPIFile := "main.go" - p := New(SetMarkdownFileDirectory(searchDir)) - p.PropNamingStrategy = PascalCase - err := p.ParseAPI(searchDir, mainAPIFile) - assert.NoError(t, err) - - if len(p.swagger.Tags) != 3 { - t.Log(len(p.swagger.Tags)) - t.Log("Number of tags did not match") - t.FailNow() - } - - dogs := p.swagger.Tags[0] - if dogs.TagProps.Name != "dogs" || dogs.TagProps.Description != "Dogs are cool" { - t.Log("Failed to parse dogs name or description") - t.FailNow() - } - - cats := p.swagger.Tags[1] - if cats.TagProps.Name != "cats" || cats.TagProps.Description != "Cats are the devil" { - t.Log("Failed to parse cats name or description") - t.FailNow() - } -} diff --git a/schema.go b/schema.go index c05a08c12..adafe3deb 100644 --- a/schema.go +++ b/schema.go @@ -1,6 +1,32 @@ package swag -import "fmt" +import ( + "errors" + "fmt" + "go/ast" + "strings" + + "github.com/go-openapi/spec" +) + +const ( + //ARRAY array + ARRAY = "array" + //OBJECT object + OBJECT = "object" + //PRIMITIVE primitive + PRIMITIVE = "primitive" + //BOOLEAN boolean + BOOLEAN = "boolean" + //INTEGER integer + INTEGER = "integer" + //NUMBER number + NUMBER = "number" + //STRING string + STRING = "string" + //FUNC func + FUNC = "func" +) // CheckSchemaType checks if typeName is not a name of primitive type func CheckSchemaType(typeName string) error { @@ -13,7 +39,7 @@ func CheckSchemaType(typeName string) error { // IsSimplePrimitiveType determine whether the type name is a simple primitive type func IsSimplePrimitiveType(typeName string) bool { switch typeName { - case "string", "number", "integer", "boolean": + case STRING, NUMBER, INTEGER, BOOLEAN: return true default: return false @@ -23,7 +49,7 @@ func IsSimplePrimitiveType(typeName string) bool { // IsPrimitiveType determine whether the type name is a primitive type func IsPrimitiveType(typeName string) bool { switch typeName { - case "string", "number", "integer", "boolean", "array", "object", "func": + case STRING, NUMBER, INTEGER, BOOLEAN, ARRAY, OBJECT, FUNC: return true default: return false @@ -32,24 +58,24 @@ func IsPrimitiveType(typeName string) bool { // IsNumericType determines whether the swagger type name is a numeric type func IsNumericType(typeName string) bool { - return typeName == "integer" || typeName == "number" + return typeName == INTEGER || typeName == NUMBER } // TransToValidSchemeType indicates type will transfer golang basic type to swagger supported type. func TransToValidSchemeType(typeName string) string { switch typeName { case "uint", "int", "uint8", "int8", "uint16", "int16", "byte": - return "integer" + return INTEGER case "uint32", "int32", "rune": - return "integer" + return INTEGER case "uint64", "int64": - return "integer" + return INTEGER case "float32", "float64": - return "number" + return NUMBER case "bool": - return "boolean" + return BOOLEAN case "string": - return "string" + return STRING default: return typeName // to support user defined types } @@ -79,3 +105,84 @@ func IsGolangPrimitiveType(typeName string) bool { return false } } + +// TransToValidCollectionFormat determine valid collection format +func TransToValidCollectionFormat(format string) string { + switch format { + case "csv", "multi", "pipes", "tsv", "ssv": + return format + default: + return "" + } +} + +// TypeDocName get alias from comment '// @name ', otherwise the original type name to display in doc +func TypeDocName(pkgName string, spec *ast.TypeSpec) string { + if spec != nil { + if spec.Comment != nil { + for _, comment := range spec.Comment.List { + text := strings.TrimSpace(comment.Text) + text = strings.TrimLeft(text, "//") + text = strings.TrimSpace(text) + texts := strings.Split(text, " ") + if len(texts) > 1 && strings.ToLower(texts[0]) == "@name" { + return texts[1] + } + } + } + if spec.Name != nil { + return fullTypeName(strings.Split(pkgName, ".")[0], spec.Name.Name) + } + } + + return pkgName +} + +//RefSchema build a reference schema +func RefSchema(refType string) *spec.Schema { + return spec.RefSchema("#/definitions/" + refType) +} + +//PrimitiveSchema build a primitive schema +func PrimitiveSchema(refType string) *spec.Schema { + return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{refType}}} +} + +// BuildCustomSchema build custom schema specified by tag swaggertype +func BuildCustomSchema(types []string) (*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 BuildCustomSchema(types[1:]) + case ARRAY: + if len(types) == 1 { + return nil, errors.New("need array item type after array") + } + schema, err := BuildCustomSchema(types[1:]) + if err != nil { + return nil, err + } + return spec.ArrayProperty(schema), nil + case OBJECT: + if len(types) == 1 { + return PrimitiveSchema(types[0]), nil + } + schema, err := BuildCustomSchema(types[1:]) + if err != nil { + return nil, err + } + return spec.MapProperty(schema), nil + default: + err := CheckSchemaType(types[0]) + if err != nil { + return nil, err + } + return PrimitiveSchema(types[0]), nil + } +} diff --git a/schema_test.go b/schema_test.go index a0993e0c9..a0d64247f 100644 --- a/schema_test.go +++ b/schema_test.go @@ -3,32 +3,44 @@ package swag import ( "testing" + "github.com/go-openapi/spec" "github.com/stretchr/testify/assert" ) func TestValidDataType(t *testing.T) { - assert.NoError(t, CheckSchemaType("string")) - assert.NoError(t, CheckSchemaType("number")) - assert.NoError(t, CheckSchemaType("integer")) - assert.NoError(t, CheckSchemaType("boolean")) - assert.NoError(t, CheckSchemaType("array")) - assert.NoError(t, CheckSchemaType("object")) + assert.NoError(t, CheckSchemaType(STRING)) + assert.NoError(t, CheckSchemaType(NUMBER)) + assert.NoError(t, CheckSchemaType(INTEGER)) + assert.NoError(t, CheckSchemaType(BOOLEAN)) + assert.NoError(t, CheckSchemaType(ARRAY)) + assert.NoError(t, CheckSchemaType(OBJECT)) assert.Error(t, CheckSchemaType("oops")) } func TestTransToValidSchemeType(t *testing.T) { - assert.Equal(t, TransToValidSchemeType("uint"), "integer") - assert.Equal(t, TransToValidSchemeType("uint32"), "integer") - assert.Equal(t, TransToValidSchemeType("uint64"), "integer") - assert.Equal(t, TransToValidSchemeType("float32"), "number") - assert.Equal(t, TransToValidSchemeType("bool"), "boolean") - assert.Equal(t, TransToValidSchemeType("string"), "string") + assert.Equal(t, TransToValidSchemeType("uint"), INTEGER) + assert.Equal(t, TransToValidSchemeType("uint32"), INTEGER) + assert.Equal(t, TransToValidSchemeType("uint64"), INTEGER) + assert.Equal(t, TransToValidSchemeType("float32"), NUMBER) + assert.Equal(t, TransToValidSchemeType("bool"), BOOLEAN) + assert.Equal(t, TransToValidSchemeType("string"), STRING) // should accept any type, due to user defined types TransToValidSchemeType("oops") } +func TestTransToValidCollectionFormat(t *testing.T) { + assert.Equal(t, TransToValidCollectionFormat("csv"), "csv") + assert.Equal(t, TransToValidCollectionFormat("multi"), "multi") + assert.Equal(t, TransToValidCollectionFormat("pipes"), "pipes") + assert.Equal(t, TransToValidCollectionFormat("tsv"), "tsv") + assert.Equal(t, TransToValidSchemeType("string"), STRING) + + // should accept any type, due to user defined types + assert.Equal(t, TransToValidCollectionFormat("oops"), "") +} + func TestIsGolangPrimitiveType(t *testing.T) { assert.Equal(t, IsGolangPrimitiveType("uint"), true) @@ -50,9 +62,66 @@ func TestIsGolangPrimitiveType(t *testing.T) { assert.Equal(t, IsGolangPrimitiveType("oops"), false) } +func TestIsSimplePrimitiveType(t *testing.T) { + + assert.Equal(t, IsSimplePrimitiveType("string"), true) + assert.Equal(t, IsSimplePrimitiveType("number"), true) + assert.Equal(t, IsSimplePrimitiveType("integer"), true) + assert.Equal(t, IsSimplePrimitiveType("boolean"), true) + + assert.Equal(t, IsSimplePrimitiveType("oops"), false) +} + +func TestBuildCustomSchema(t *testing.T) { + var schema *spec.Schema + var err error + + schema, err = BuildCustomSchema([]string{}) + assert.NoError(t, err) + assert.Nil(t, schema) + + schema, err = BuildCustomSchema([]string{"primitive"}) + assert.Error(t, err) + assert.Nil(t, schema) + + schema, err = BuildCustomSchema([]string{"primitive", "oops"}) + assert.Error(t, err) + assert.Nil(t, schema) + + schema, err = BuildCustomSchema([]string{"primitive", "string"}) + assert.NoError(t, err) + assert.Equal(t, schema.SchemaProps.Type, spec.StringOrArray{"string"}) + + schema, err = BuildCustomSchema([]string{"array"}) + assert.Error(t, err) + assert.Nil(t, schema) + + schema, err = BuildCustomSchema([]string{"array", "oops"}) + assert.Error(t, err) + assert.Nil(t, schema) + + schema, err = BuildCustomSchema([]string{"array", "string"}) + assert.NoError(t, err) + assert.Equal(t, schema.SchemaProps.Type, spec.StringOrArray{"array"}) + assert.Equal(t, schema.SchemaProps.Items.Schema.SchemaProps.Type, spec.StringOrArray{"string"}) + + schema, err = BuildCustomSchema([]string{"object"}) + assert.NoError(t, err) + assert.Equal(t, schema.SchemaProps.Type, spec.StringOrArray{"object"}) + + schema, err = BuildCustomSchema([]string{"object", "oops"}) + assert.Error(t, err) + assert.Nil(t, schema) + + schema, err = BuildCustomSchema([]string{"object", "string"}) + assert.NoError(t, err) + assert.Equal(t, schema.SchemaProps.Type, spec.StringOrArray{"object"}) + assert.Equal(t, schema.SchemaProps.AdditionalProperties.Schema.Type, spec.StringOrArray{"string"}) +} + func TestIsNumericType(t *testing.T) { - assert.Equal(t, IsNumericType("integer"), true) - assert.Equal(t, IsNumericType("number"), true) + assert.Equal(t, IsNumericType(INTEGER), true) + assert.Equal(t, IsNumericType(NUMBER), true) - assert.Equal(t, IsNumericType("string"), false) + assert.Equal(t, IsNumericType(STRING), false) } diff --git a/testdata/alias_import/api/api.go b/testdata/alias_import/api/api.go index 0e24583f3..a01a3ddd5 100644 --- a/testdata/alias_import/api/api.go +++ b/testdata/alias_import/api/api.go @@ -1,10 +1,11 @@ package api import ( - "github.com/gin-gonic/gin" + "log" + "net/http" + "github.com/swaggo/swag/testdata/alias_import/data" "github.com/swaggo/swag/testdata/alias_type/types" - "log" ) // @Summary Get application @@ -14,7 +15,7 @@ import ( // @Produce json // @Success 200 {object} data.ApplicationResponse "ok" // @Router /testapi/application [get] -func GetApplication(c *gin.Context) { +func GetApplication(w http.ResponseWriter, r *http.Request) { var foo = data.ApplicationResponse{ Application: types.Application{ Name: "name", diff --git a/testdata/alias_import/data/applicationresponse.go b/testdata/alias_import/data/applicationresponse.go index b6bcf3c58..da4170508 100644 --- a/testdata/alias_import/data/applicationresponse.go +++ b/testdata/alias_import/data/applicationresponse.go @@ -5,6 +5,8 @@ import ( ) type ApplicationResponse struct { + typesapplication.TypeToEmbed + Application typesapplication.Application `json:"application"` ApplicationArray []typesapplication.Application `json:"application_array"` ApplicationTime typesapplication.DateOnly `json:"application_time"` diff --git a/testdata/alias_import/expected.json b/testdata/alias_import/expected.json index 4b6232987..fab871b41 100644 --- a/testdata/alias_import/expected.json +++ b/testdata/alias_import/expected.json @@ -45,7 +45,6 @@ "type": "object", "properties": { "application": { - "type": "object", "$ref": "#/definitions/types.Application" }, "application_array": { @@ -56,6 +55,9 @@ }, "application_time": { "type": "string" + }, + "embedded": { + "type": "string" } } }, diff --git a/testdata/alias_import/main.go b/testdata/alias_import/main.go index bc8768d94..bf1b5c93b 100644 --- a/testdata/alias_import/main.go +++ b/testdata/alias_import/main.go @@ -1,7 +1,8 @@ package alias_import import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/testdata/alias_import/api" ) @@ -20,7 +21,6 @@ import ( // @host petstore.swagger.io // @BasePath /v2 func main() { - r := gin.New() - r.GET("/testapi/application", api.GetApplication) - r.Run() + http.HandleFunc("/testapi/application", api.GetApplication) + http.ListenAndServe(":8080", nil) } diff --git a/testdata/alias_import/types/application.go b/testdata/alias_import/types/application.go index ceb7120ec..ddff98ef0 100644 --- a/testdata/alias_import/types/application.go +++ b/testdata/alias_import/types/application.go @@ -7,3 +7,7 @@ type Application struct { } type DateOnly time.Time + +type TypeToEmbed struct { + Embedded string +} diff --git a/testdata/alias_type/api/api.go b/testdata/alias_type/api/api.go index 1d6199436..3c9638e06 100644 --- a/testdata/alias_type/api/api.go +++ b/testdata/alias_type/api/api.go @@ -1,10 +1,11 @@ package api import ( - "github.com/gin-gonic/gin" - "github.com/swaggo/swag/testdata/alias_type/data" "log" + "net/http" "time" + + "github.com/swaggo/swag/testdata/alias_type/data" ) /*// @Summary Get time as string @@ -14,7 +15,7 @@ import ( // @Produce json // @Success 200 {object} data.StringAlias "ok" // @Router /testapi/time-as-string [get] -func GetTimeAsStringAlias(c *gin.Context) { +func GetTimeAsStringAlias(w http.ResponseWriter, r *http.Request) { var foo data.StringAlias = "test" log.Println(foo) //write your code @@ -27,7 +28,7 @@ func GetTimeAsStringAlias(c *gin.Context) { // @Produce json // @Success 200 {object} data.DateOnly "ok" // @Router /testapi/time-as-time [get] -func GetTimeAsTimeAlias(c *gin.Context) { +func GetTimeAsTimeAlias(w http.ResponseWriter, r *http.Request) { var foo = data.DateOnly(time.Now()) log.Println(foo) //write your code @@ -40,7 +41,7 @@ func GetTimeAsTimeAlias(c *gin.Context) { // @Produce json // @Success 200 {object} data.TimeContainer "ok" // @Router /testapi/time-as-time-container [get] -func GetTimeAsTimeContainer(c *gin.Context) { +func GetTimeAsTimeContainer(w http.ResponseWriter, r *http.Request) { now := time.Now() var foo = data.TimeContainer{ Name: "test", diff --git a/testdata/alias_type/main.go b/testdata/alias_type/main.go index 94affaf99..f136fbb95 100644 --- a/testdata/alias_type/main.go +++ b/testdata/alias_type/main.go @@ -1,7 +1,8 @@ package alias_type import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/testdata/alias_type/api" ) @@ -20,7 +21,6 @@ import ( // @host petstore.swagger.io // @BasePath /v2 func main() { - r := gin.New() - r.GET("/testapi/time-as-time-container", api.GetTimeAsTimeContainer) - r.Run() + http.HandleFunc("/testapi/time-as-time-container", api.GetTimeAsTimeContainer) + http.ListenAndServe(":8080", nil) } diff --git a/testdata/code_examples/api/api1.go b/testdata/code_examples/api/api1.go new file mode 100644 index 000000000..4bbd5512f --- /dev/null +++ b/testdata/code_examples/api/api1.go @@ -0,0 +1,13 @@ +package api + +import ( + _ "github.com/swaggo/swag/testdata/conflict_name/model" + "net/http" +) + +// @Description Check if Health of service it's OK! +// @Router /health [get] +// @x-codeSamples file +func Get1(w http.ResponseWriter, r *http.Request) { + +} diff --git a/testdata/code_examples/example.json b/testdata/code_examples/example.json new file mode 100644 index 000000000..26e1cef56 --- /dev/null +++ b/testdata/code_examples/example.json @@ -0,0 +1,4 @@ +{ + "lang": "JavaScript", + "source": "console.log('Hello World');" +} \ No newline at end of file diff --git a/testdata/code_examples/main.go b/testdata/code_examples/main.go new file mode 100644 index 000000000..476d5baa2 --- /dev/null +++ b/testdata/code_examples/main.go @@ -0,0 +1,8 @@ +package main + +// @title Swag test +// @version 1.0 +// @description test for conflict name +func main() { + +} diff --git a/testdata/composition/api/api.go b/testdata/composition/api/api.go index 0845b6b92..bd3c62487 100644 --- a/testdata/composition/api/api.go +++ b/testdata/composition/api/api.go @@ -1,7 +1,8 @@ package api import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/testdata/composition/common" ) @@ -11,16 +12,29 @@ type Foo struct { type Bar struct { Field2 string } +type EmptyStruct struct { +} +type unexported struct { +} +type Ignored struct { + Field5 string `swaggerignore:"true"` +} type FooBar struct { Foo Bar + EmptyStruct + unexported + Ignored } type FooBarPointer struct { *common.ResponseFormat *Foo *Bar + *EmptyStruct + *unexported + *Ignored } type BarMap map[string]Bar @@ -39,7 +53,7 @@ type MapValue struct { // @Produce json // @Success 200 {object} api.Foo // @Router /testapi/get-foo [get] -func GetFoo(c *gin.Context) { +func GetFoo(w http.ResponseWriter, r *http.Request) { //write your code var _ = Foo{} } @@ -50,7 +64,7 @@ func GetFoo(c *gin.Context) { // @Produce json // @Success 200 {object} api.Bar // @Router /testapi/get-bar [get] -func GetBar(c *gin.Context) { +func GetBar(w http.ResponseWriter, r *http.Request) { //write your code var _ = Bar{} } @@ -61,7 +75,7 @@ func GetBar(c *gin.Context) { // @Produce json // @Success 200 {object} api.FooBar // @Router /testapi/get-foobar [get] -func GetFooBar(c *gin.Context) { +func GetFooBar(w http.ResponseWriter, r *http.Request) { //write your code var _ = FooBar{} } @@ -72,7 +86,7 @@ func GetFooBar(c *gin.Context) { // @Produce json // @Success 200 {object} api.FooBarPointer // @Router /testapi/get-foobar-pointer [get] -func GetFooBarPointer(c *gin.Context) { +func GetFooBarPointer(w http.ResponseWriter, r *http.Request) { //write your code var _ = FooBarPointer{} } @@ -83,7 +97,7 @@ func GetFooBarPointer(c *gin.Context) { // @Produce json // @Success 200 {object} api.BarMap // @Router /testapi/get-barmap [get] -func GetBarMap(c *gin.Context) { +func GetBarMap(w http.ResponseWriter, r *http.Request) { //write your code var _ = BarMap{} } @@ -94,7 +108,7 @@ func GetBarMap(c *gin.Context) { // @Produce json // @Success 200 {object} api.FooBarMap // @Router /testapi/get-foobarmap [get] -func GetFooBarMap(c *gin.Context) { +func GetFooBarMap(w http.ResponseWriter, r *http.Request) { //write your code var _ = FooBarMap{} } diff --git a/testdata/composition/expected.json b/testdata/composition/expected.json index d14bfbd91..f260e066c 100644 --- a/testdata/composition/expected.json +++ b/testdata/composition/expected.json @@ -5,7 +5,6 @@ "title": "Swagger Example API", "termsOfService": "http://swagger.io/terms/", "contact": {}, - "license": {}, "version": "1.0" }, "host": "petstore.swagger.io", diff --git a/testdata/composition/main.go b/testdata/composition/main.go index 23e08ee00..e0dc20d64 100644 --- a/testdata/composition/main.go +++ b/testdata/composition/main.go @@ -1,7 +1,8 @@ package composition import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/testdata/composition/api" ) @@ -14,11 +15,10 @@ import ( // @BasePath /v2 func main() { - r := gin.New() - r.GET("/testapi/get-foo", api.GetFoo) - r.GET("/testapi/get-bar", api.GetBar) - r.GET("/testapi/get-foobar", api.GetFooBar) - r.GET("/testapi/get-foobar-pointer", api.GetFooBarPointer) - r.GET("/testapi/get-barmap", api.GetBarMap) - r.Run() + http.handleFunc("/testapi/get-foo", api.GetFoo) + http.handleFunc("/testapi/get-bar", api.GetBar) + http.handleFunc("/testapi/get-foobar", api.GetFooBar) + http.handleFunc("/testapi/get-foobar-pointer", api.GetFooBarPointer) + http.handleFunc("/testapi/get-barmap", api.GetBarMap) + http.ListenAndServe(":8080", nil) } diff --git a/testdata/conflict_name/api/api1.go b/testdata/conflict_name/api/api1.go new file mode 100644 index 000000000..7b04198a7 --- /dev/null +++ b/testdata/conflict_name/api/api1.go @@ -0,0 +1,17 @@ +package api + +import ( + _ "github.com/swaggo/swag/testdata/conflict_name/model" + "net/http" +) + +// @Tags Health +// @Description Check if Health of service it's OK! +// @ID health +// @Accept json +// @Produce json +// @Success 200 {object} model.ErrorsResponse +// @Router /health [get] +func Get1(w http.ResponseWriter, r *http.Request) { + +} diff --git a/testdata/conflict_name/api/api2.go b/testdata/conflict_name/api/api2.go new file mode 100644 index 000000000..e71b7e6c1 --- /dev/null +++ b/testdata/conflict_name/api/api2.go @@ -0,0 +1,17 @@ +package api + +import ( + _ "github.com/swaggo/swag/testdata/conflict_name/model2" + "net/http" +) + +// @Tags Health +// @Description Check if Health of service it's OK! +// @ID health2 +// @Accept json +// @Produce json +// @Success 200 {object} model.ErrorsResponse +// @Router /health2 [get] +func Get2(w http.ResponseWriter, r *http.Request) { + +} diff --git a/testdata/conflict_name/expected.json b/testdata/conflict_name/expected.json new file mode 100644 index 000000000..74f046c46 --- /dev/null +++ b/testdata/conflict_name/expected.json @@ -0,0 +1,113 @@ +{ + "swagger": "2.0", + "info": { + "description": "test for conflict name", + "title": "Swag test", + "contact": {}, + "version": "1.0" + }, + "paths": { + "/health": { + "get": { + "description": "Check if Health of service it's OK!", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Health" + ], + "operationId": "health", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github.com_swaggo_swag_testdata_conflict_name_model.ErrorsResponse" + } + } + } + } + }, + "/health2": { + "get": { + "description": "Check if Health of service it's OK!", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Health" + ], + "operationId": "health2", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github.com_swaggo_swag_testdata_conflict_name_model2.ErrorsResponse" + } + } + } + } + } + }, + "definitions": { + "github.com_swaggo_swag_testdata_conflict_name_model.ErrorsResponse": { + "type": "object", + "properties": { + "newTime": { + "$ref": "#/definitions/model.MyPayload" + } + } + }, + "github.com_swaggo_swag_testdata_conflict_name_model.MyStruct": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "github.com_swaggo_swag_testdata_conflict_name_model2.ErrorsResponse": { + "type": "object", + "properties": { + "newTime": { + "$ref": "#/definitions/model.MyPayload2" + } + } + }, + "github.com_swaggo_swag_testdata_conflict_name_model2.MyStruct": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "model.MyPayload": { + "type": "object", + "properties": { + "my": { + "$ref": "#/definitions/github.com_swaggo_swag_testdata_conflict_name_model.MyStruct" + }, + "name": { + "type": "string" + } + } + }, + "model.MyPayload2": { + "type": "object", + "properties": { + "my": { + "$ref": "#/definitions/github.com_swaggo_swag_testdata_conflict_name_model2.MyStruct" + }, + "name": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/testdata/conflict_name/main.go b/testdata/conflict_name/main.go new file mode 100644 index 000000000..476d5baa2 --- /dev/null +++ b/testdata/conflict_name/main.go @@ -0,0 +1,8 @@ +package main + +// @title Swag test +// @version 1.0 +// @description test for conflict name +func main() { + +} diff --git a/testdata/conflict_name/model/model.go b/testdata/conflict_name/model/model.go new file mode 100644 index 000000000..a55440ee6 --- /dev/null +++ b/testdata/conflict_name/model/model.go @@ -0,0 +1,14 @@ +package model + +type MyStruct struct { + Name string `json:"name"` +} + +type MyPayload struct { + My MyStruct + Name string `json:"name"` +} + +type ErrorsResponse struct { + NewTime MyPayload +} diff --git a/testdata/conflict_name/model2/model.go b/testdata/conflict_name/model2/model.go new file mode 100644 index 000000000..bd67e781e --- /dev/null +++ b/testdata/conflict_name/model2/model.go @@ -0,0 +1,14 @@ +package model + +type MyStruct struct { + Name string `json:"name"` +} + +type MyPayload2 struct { + My MyStruct + Name string `json:"name"` +} + +type ErrorsResponse struct { + NewTime MyPayload2 +} diff --git a/testdata/duplicated/api/api.go b/testdata/duplicated/api/api.go new file mode 100644 index 000000000..829a43048 --- /dev/null +++ b/testdata/duplicated/api/api.go @@ -0,0 +1,15 @@ +package api + +import "net/http" + +// @Description get Foo +// @ID get-foo +// @Success 200 {string} string +// @Router /testapi/get-foo [get] +func GetFoo(w http.ResponseWriter, r *http.Request) {} + +// @Description post Bar +// @ID get-foo +// @Success 200 {string} string +// @Router /testapi/post-bar [post] +func PostBar(w http.ResponseWriter, r *http.Request) {} diff --git a/testdata/duplicated/main.go b/testdata/duplicated/main.go new file mode 100644 index 000000000..d1e16664f --- /dev/null +++ b/testdata/duplicated/main.go @@ -0,0 +1,21 @@ +package composition + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/duplicated/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server +// @termsOfService http://swagger.io/terms/ + +// @host petstore.swagger.io +// @BasePath /v2 + +func main() { + http.HandleFunc("/testapi/get-foo", api.GetFoo) + http.HandleFunc("/testapi/post-bar", api.PostBar) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/duplicated2/api/api.go b/testdata/duplicated2/api/api.go new file mode 100644 index 000000000..ef1fcc29c --- /dev/null +++ b/testdata/duplicated2/api/api.go @@ -0,0 +1,33 @@ +package api + +import "net/http" + +// @Description put Foo +// @ID put-foo +// @Success 200 {string} string +// @Router /testapi/put-foo [put] +func PutFoo(w http.ResponseWriter, r *http.Request) {} + +// @Description head Foo +// @ID head-foo +// @Success 200 {string} string +// @Router /testapi/head-foo [head] +func HeadFoo(w http.ResponseWriter, r *http.Request) {} + +// @Description options Foo +// @ID options-foo +// @Success 200 {string} string +// @Router /testapi/options-foo [options] +func OptionsFoo(w http.ResponseWriter, r *http.Request) {} + +// @Description patch Foo +// @ID patch-foo +// @Success 200 {string} string +// @Router /testapi/patch-foo [patch] +func PatchFoo(w http.ResponseWriter, r *http.Request) {} + +// @Description delete Foo +// @ID put-foo +// @Success 200 {string} string +// @Router /testapi/delete-foo [delete] +func DeleteFoo(w http.ResponseWriter, r *http.Request) {} diff --git a/testdata/duplicated2/main.go b/testdata/duplicated2/main.go new file mode 100644 index 000000000..90988f76d --- /dev/null +++ b/testdata/duplicated2/main.go @@ -0,0 +1,24 @@ +package composition + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/duplicated2/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server +// @termsOfService http://swagger.io/terms/ + +// @host petstore.swagger.io +// @BasePath /v2 + +func main() { + http.HandleFunc("/testapi/put-foo", api.PutFoo) + http.HandleFunc("/testapi/head-foo", api.HeadFoo) + http.HandleFunc("/testapi/options-foo", api.OptionsFoo) + http.HandleFunc("/testapi/patch-foo", api.PatchFoo) + http.HandleFunc("/testapi/delete-foo", api.DeleteFoo) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/json_field_string/main.go b/testdata/json_field_string/main.go new file mode 100755 index 000000000..564503a2a --- /dev/null +++ b/testdata/json_field_string/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type MyStruct struct { + ID int `json:"id" example:"1" format:"int64"` + Name string `json:"name" example:"poti"` + Intvar int `json:"myint,string"` // integer as string + Boolvar bool `json:",string"` // boolean as a string + TrueBool bool `json:"truebool,string" example:"true"` // boolean as a string + Floatvar float64 `json:",string"` // float as a string +} + +// @Summary Call DoSomething +// @Description Does something +// @Accept json +// @Produce json +// @Param body body MyStruct true "My Struct" +// @Success 200 {object} MyStruct +// @Failure 500 +// @Router /do-something [post] +func DoSomething(w http.ResponseWriter, r *http.Request) { + objectFromJSON := new(MyStruct) + if err := json.NewDecoder(r.Body).Decode(&objectFromJSON); err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Print(err.Error()) + } + json.NewEncoder(w).Encode(ojbectFromJSON) +} + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server. +// @host localhost:4000 +// @basePath / +func main() { + http.HandleFund("/do-something", DoSomething) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/main.go b/testdata/main.go index f6ef4e21f..9ee7c9339 100644 --- a/testdata/main.go +++ b/testdata/main.go @@ -42,8 +42,10 @@ package main // @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 // @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/model_not_under_root/cmd/api/api.go b/testdata/model_not_under_root/cmd/api/api.go deleted file mode 100644 index efc56c736..000000000 --- a/testdata/model_not_under_root/cmd/api/api.go +++ /dev/null @@ -1,34 +0,0 @@ -package api - -import ( - "log" - - "github.com/gin-gonic/gin" - "github.com/swaggo/swag/testdata/model_not_under_root/data" -) - -// @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) -// @Success 200 {object} data.Foo "ok" -// @Router /testapi/get-string-by-int/{some_id} [get] -func GetStringByInt(c *gin.Context) { - var foo data.Foo - log.Println(foo) - //write your code -} - -// @Summary Upload file -// @Description Upload file -// @ID file.upload -// @Accept json -// @Produce json -// @Param data body data.Foo true "Foo to create" -// @Success 200 {string} string "ok" -// @Router /file/upload [post] -func Upload(ctx *gin.Context) { - //write your code -} diff --git a/testdata/model_not_under_root/data/foo.go b/testdata/model_not_under_root/data/foo.go deleted file mode 100644 index 7fe7208f9..000000000 --- a/testdata/model_not_under_root/data/foo.go +++ /dev/null @@ -1,5 +0,0 @@ -package data - -type Foo struct { - Field1 string `json:"field1"` -} diff --git a/testdata/nested/api/api.go b/testdata/nested/api/api.go index e7869da85..0dec600ac 100644 --- a/testdata/nested/api/api.go +++ b/testdata/nested/api/api.go @@ -1,7 +1,8 @@ package api import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/testdata/nested2" ) @@ -23,7 +24,7 @@ type Bar struct { // @Produce json // @Success 200 {object} api.Foo // @Router /testapi/get-foo [get] -func GetFoo(c *gin.Context) { +func GetFoo(w http.ResponseWriter, r *http.Request) { //write your code var _ = Foo{} } diff --git a/testdata/nested/expected.json b/testdata/nested/expected.json index e6ebb1cc4..6ed9d51b1 100644 --- a/testdata/nested/expected.json +++ b/testdata/nested/expected.json @@ -5,7 +5,6 @@ "title": "Swagger Example API", "termsOfService": "http://swagger.io/terms/", "contact": {}, - "license": {}, "version": "1.0" }, "host": "petstore.swagger.io", @@ -66,11 +65,9 @@ "type": "string" }, "insideData": { - "type": "object", "$ref": "#/definitions/api.Bar" }, "outsideData": { - "type": "object", "$ref": "#/definitions/nested2.Body" } } diff --git a/testdata/nested/main.go b/testdata/nested/main.go index 1649b9bbb..6e63913b3 100644 --- a/testdata/nested/main.go +++ b/testdata/nested/main.go @@ -1,7 +1,8 @@ package composition import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/testdata/nested/api" ) @@ -14,7 +15,6 @@ import ( // @BasePath /v2 func main() { - r := gin.New() - r.GET("/testapi/get-foo", api.GetFoo) - r.Run() + http.HandleFunc("/testapi/get-foo", api.GetFoo) + http.ListenAndServe(":8080", nil) } diff --git a/testdata/nested2/inner/data.go b/testdata/nested2/inner/data.go new file mode 100644 index 000000000..8a7eab5c4 --- /dev/null +++ b/testdata/nested2/inner/data.go @@ -0,0 +1 @@ +package inner diff --git a/testdata/non_exported_json_fields/main.go b/testdata/non_exported_json_fields/main.go new file mode 100644 index 000000000..b02dcc56c --- /dev/null +++ b/testdata/non_exported_json_fields/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "net/http" +) + +type MyStruct struct { + ID int `json:"id" example:"1" format:"int64"` + // Post name + Name string `json:"name" example:"poti"` + // Post data + Data struct { + // Post tag + Tag []string `json:"name"` + } `json:"data"` + // not-exported variable, for internal use only, not marshaled + internal1 string + internal2 int + internal3 bool + internal4 struct { + NestedInternal string + } +} + +// @Summary Call DoSomething +// @Description Does something, but internal (non-exported) fields inside a struct won't be marshaled into JSON +// @Accept json +// @Produce json +// @Success 200 {object} MyStruct +// @Router /so-something [get] +func DoSomething(w http.ResponseWriter, r *http.Request) { + //write your code +} + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server. +// @host localhost:4000 +// @basePath /api +func main() { + http.HandleFunc("/do-something", DoSomething) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/pare_outside_dependencies/cmd/main.go b/testdata/pare_outside_dependencies/cmd/main.go index a93ea32c3..9f22eb586 100644 --- a/testdata/pare_outside_dependencies/cmd/main.go +++ b/testdata/pare_outside_dependencies/cmd/main.go @@ -1,7 +1,8 @@ package main import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/example/basic/api" ) @@ -20,10 +21,8 @@ import ( // @host petstore.swagger.io // @BasePath /v2 func main() { - r := gin.New() - r.GET("/testapi/get-string-by-int/:some_id", api.GetStringByInt) - r.GET("//testapi/get-struct-array-by-string/:some_id", api.GetStructArrayByString) - r.POST("/testapi/upload", api.Upload) - r.Run() - + 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/simple/api/api.go b/testdata/simple/api/api.go index ba6734d71..bde8f703b 100644 --- a/testdata/simple/api/api.go +++ b/testdata/simple/api/api.go @@ -1,7 +1,9 @@ package api import ( - "github.com/gin-gonic/gin" + "net/http" + + _ "github.com/swaggo/swag/testdata/simple/web" ) // @Summary Add a new pet to the store @@ -15,7 +17,7 @@ import ( // @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(c *gin.Context) { +func GetStringByInt(w http.ResponseWriter, r *http.Request) { //write your code } @@ -25,8 +27,8 @@ func GetStringByInt(c *gin.Context) { // @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" Mininum(0) default(0) -// @Param limit query int true "Limit" Maxinum(50) default(10) +// @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!!" @@ -38,7 +40,7 @@ func GetStringByInt(c *gin.Context) { // @Security OAuth2AccessCode[read] // @Security OAuth2Password[admin] // @Router /testapi/get-struct-array-by-string/{some_id} [get] -func GetStructArrayByString(c *gin.Context) { +func GetStructArrayByString(w http.ResponseWriter, r *http.Request) { //write your code } @@ -53,39 +55,45 @@ func GetStructArrayByString(c *gin.Context) { // @Failure 401 {array} string // @Failure 404 {object} web.APIError "Can not find ID" // @Router /file/upload [post] -func Upload(ctx *gin.Context) { +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() { } @@ -94,16 +102,19 @@ type Pet3 struct { } // @Success 200 {object} web.Pet5a "ok" +// @Router /GetPet5a [get] func GetPet5a() { } // @Success 200 {object} web.Pet5b "ok" +// @Router /GetPet5b [get] func GetPet5b() { } // @Success 200 {object} web.Pet5c "ok" +// @Router /GetPet5c [get] func GetPet5c() { } @@ -111,6 +122,7 @@ func GetPet5c() { type SwagReturn []map[string]string // @Success 200 {object} api.SwagReturn "ok" +// @Router /GetPet6MapString [get] func GetPet6MapString() { } diff --git a/testdata/simple/expected.json b/testdata/simple/expected.json new file mode 100644 index 000000000..efd201783 --- /dev/null +++ b/testdata/simple/expected.json @@ -0,0 +1,739 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server.", + "title": "Swagger Example API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "paths": { + "/AnonymousField": { + "get": { + "summary": "use Anonymous field", + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.RevValue" + } + } + } + } + }, + "/AnonymousStructArray": { + "get": { + "summary": "Use AnonymousStructArray", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + } + } + } + } + }, + "/CrossAlias": { + "get": { + "summary": "Use CrossAlias", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.CrossAlias" + } + } + } + } + }, + "/GetPet5a": { + "get": { + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.Pet5a" + } + } + } + } + }, + "/GetPet5b": { + "get": { + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.Pet5b" + } + } + } + } + }, + "/GetPet5c": { + "get": { + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.Pet5c" + } + } + } + } + }, + "/GetPet6MapString": { + "get": { + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "/IndirectRecursiveTest": { + "get": { + "summary": "Use IndirectRecursiveTest", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.IndirectRecursiveTest" + } + } + } + } + }, + "/Pet2": { + "get": { + "summary": "use pet2", + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.Pet2" + } + } + } + } + }, + "/Tags": { + "get": { + "summary": "Use Tags", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Tag" + } + } + } + } + } + }, + "/file/upload": { + "post": { + "description": "Upload file", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "summary": "Upload file", + "operationId": "file.upload", + "parameters": [ + { + "type": "file", + "description": "this is a test file", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "We need ID!!", + "schema": { + "$ref": "#/definitions/web.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "404": { + "description": "Can not find ID", + "schema": { + "$ref": "#/definitions/web.APIError" + } + } + } + } + }, + "/testapi/get-string-by-int/{some_id}": { + "get": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add a new pet to the store", + "operationId": "get-string-by-int", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Some ID", + "name": "some_id", + "in": "path", + "required": true + }, + { + "description": "Some ID", + "name": "some_id", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/web.Pet" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "We need ID!!", + "schema": { + "$ref": "#/definitions/web.APIError" + } + }, + "404": { + "description": "Can not find ID", + "schema": { + "$ref": "#/definitions/web.APIError" + } + } + } + } + }, + "/testapi/get-struct-array-by-string/{some_id}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BasicAuth": [] + }, + { + "OAuth2Application": [ + "write" + ] + }, + { + "OAuth2Implicit": [ + "read", + "admin" + ] + }, + { + "OAuth2AccessCode": [ + "read" + ] + }, + { + "OAuth2Password": [ + "admin" + ] + } + ], + "description": "get struct array by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "operationId": "get-struct-array-by-string", + "parameters": [ + { + "type": "string", + "description": "Some ID", + "name": "some_id", + "in": "path", + "required": true + }, + { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "description": "Category", + "name": "category", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query", + "required": true + }, + { + "maximum": 50, + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query", + "required": true + }, + { + "maxLength": 50, + "minLength": 1, + "type": "string", + "default": "\"\"", + "description": "q", + "name": "q", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "We need ID!!", + "schema": { + "$ref": "#/definitions/web.APIError" + } + }, + "404": { + "description": "Can not find ID", + "schema": { + "$ref": "#/definitions/web.APIError" + } + } + } + } + } + }, + "definitions": { + "cross.Cross": { + "type": "object", + "properties": { + "Array": { + "type": "array", + "items": { + "type": "string" + } + }, + "String": { + "type": "string" + } + } + }, + "web.APIError": { + "type": "object", + "properties": { + "CreatedAt": { + "type": "string" + }, + "ErrorCode": { + "type": "integer" + }, + "ErrorMessage": { + "type": "string" + } + } + }, + "web.CrossAlias": { + "type": "object", + "properties": { + "Array": { + "type": "array", + "items": { + "type": "string" + } + }, + "String": { + "type": "string" + } + } + }, + "web.IndirectRecursiveTest": { + "type": "object", + "properties": { + "Tags": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Tag" + } + } + } + }, + "web.Pet": { + "type": "object", + "required": [ + "name", + "photo_urls" + ], + "properties": { + "category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "category_name" + }, + "photo_urls": { + "type": "array", + "format": "url", + "items": { + "type": "string" + }, + "example": [ + "http://test/image/1.jpg", + "http://test/image/2.jpg" + ] + }, + "small_category": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "maxLength": 16, + "minLength": 4, + "example": "detail_category_name" + }, + "photo_urls": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "http://test/image/1.jpg", + "http://test/image/2.jpg" + ] + } + } + } + } + }, + "data": { + "type": "object" + }, + "decimal": { + "type": "number" + }, + "enum_array": { + "type": "array", + "items": { + "type": "integer", + "enum": [ + 1, + 2, + 3, + 5, + 7 + ] + } + }, + "id": { + "type": "integer", + "format": "int64", + "readOnly": true, + "example": 1 + }, + "int_array": { + "type": "array", + "items": { + "type": "integer" + }, + "example": [ + 1, + 2 + ] + }, + "is_alive": { + "type": "boolean", + "default": true, + "example": true + }, + "name": { + "type": "string", + "example": "poti" + }, + "pets": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Pet2" + } + }, + "pets2": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Pet2" + } + }, + "photo_urls": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "http://test/image/1.jpg", + "http://test/image/2.jpg" + ] + }, + "price": { + "type": "number", + "maximum": 1000, + "minimum": 1, + "example": 3.25 + }, + "status": { + "type": "string", + "enum": [ + "healthy", + "ill" + ] + }, + "string_map": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "key1": "value", + "key2": "value2" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Tag" + } + }, + "uuid": { + "type": "string" + } + } + }, + "web.Pet2": { + "type": "object", + "properties": { + "deleted_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "middlename": { + "type": "string", + "x-abc": "def", + "x-nullable": true + } + } + }, + "web.Pet5a": { + "type": "object", + "required": [ + "name", + "odd" + ], + "properties": { + "name": { + "type": "string" + }, + "odd": { + "type": "boolean" + } + } + }, + "web.Pet5b": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "web.Pet5c": { + "type": "object", + "required": [ + "name", + "odd" + ], + "properties": { + "name": { + "type": "string" + }, + "odd": { + "type": "boolean" + } + } + }, + "web.RevValue": { + "type": "object", + "properties": { + "Data": { + "type": "integer" + }, + "Err": { + "type": "integer" + }, + "Status": { + "type": "boolean" + }, + "cross": { + "$ref": "#/definitions/cross.Cross" + }, + "crosses": { + "type": "array", + "items": { + "$ref": "#/definitions/cross.Cross" + } + } + } + }, + "web.Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "pets": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Pet" + } + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "BasicAuth": { + "type": "basic" + }, + "OAuth2AccessCode": { + "type": "oauth2", + "flow": "accessCode", + "authorizationUrl": "https://example.com/oauth/authorize", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "admin": " Grants read and write access to administrative information" + } + }, + "OAuth2Application": { + "type": "oauth2", + "flow": "application", + "authorizationUrl": "", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "admin": " Grants read and write access to administrative information", + "write": " Grants write access" + } + }, + "OAuth2Implicit": { + "type": "oauth2", + "flow": "implicit", + "authorizationUrl": "https://example.com/oauth/authorize", + "scopes": { + "admin": " Grants read and write access to administrative information", + "write": " Grants write access" + } + }, + "OAuth2Password": { + "type": "oauth2", + "flow": "password", + "authorizationUrl": "", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "admin": " Grants read and write access to administrative information", + "read": " Grants read access", + "write": " Grants write access" + } + } + } +} \ No newline at end of file diff --git a/testdata/simple/main.go b/testdata/simple/main.go index d68402270..824a017c5 100644 --- a/testdata/simple/main.go +++ b/testdata/simple/main.go @@ -1,7 +1,8 @@ package main import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/testdata/simple/api" ) @@ -47,9 +48,8 @@ import ( // @authorizationurl https://example.com/oauth/authorize // @scope.admin Grants read and write access to administrative information func main() { - r := gin.New() - r.GET("/testapi/get-string-by-int/:some_id", api.GetStringByInt) - r.GET("/testapi/get-struct-array-by-string/:some_id", api.GetStructArrayByString) - r.POST("/testapi/upload", api.Upload) - r.Run() + 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/simple/web/handler.go b/testdata/simple/web/handler.go index ece1628dc..6c4d39e43 100644 --- a/testdata/simple/web/handler.go +++ b/testdata/simple/web/handler.go @@ -3,7 +3,7 @@ package web import ( "time" - "github.com/satori/go.uuid" + "github.com/gofrs/uuid" "github.com/shopspring/decimal" "github.com/swaggo/swag/testdata/simple/cross" ) @@ -20,20 +20,21 @@ type Pet struct { 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"` - 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"` - EnumArray []int `json:"enum_array" enums:"1,2,3,5,7"` + 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"` + 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"` } type Tag struct { diff --git a/testdata/simple2/api/api.go b/testdata/simple2/api/api.go index bed956f38..f65c534c1 100644 --- a/testdata/simple2/api/api.go +++ b/testdata/simple2/api/api.go @@ -1,7 +1,7 @@ package api import ( - "github.com/gin-gonic/gin" + "net/http" ) // @Summary Add a new pet to the store @@ -15,7 +15,7 @@ import ( // @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(c *gin.Context) { +func GetStringByInt(w http.ResponseWriter, r *http.Request) { //write your code } @@ -25,8 +25,8 @@ func GetStringByInt(c *gin.Context) { // @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" Mininum(0) default(0) -// @Param limit query int true "Limit" Maxinum(50) default(10) +// @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!!" @@ -38,7 +38,7 @@ func GetStringByInt(c *gin.Context) { // @Security OAuth2AccessCode[read] // @Security OAuth2Password[admin] // @Router /testapi/get-struct-array-by-string/{some_id} [get] -func GetStructArrayByString(c *gin.Context) { +func GetStructArrayByString(w http.ResponseWriter, r *http.Request) { //write your code } @@ -52,7 +52,7 @@ func GetStructArrayByString(c *gin.Context) { // @Failure 400 {object} web.APIError "We need ID!!" // @Failure 404 {object} web.APIError "Can not find ID" // @Router /file/upload [post] -func Upload(ctx *gin.Context) { +func Upload(w http.ResponseWriter, r *http.Request) { //write your code } diff --git a/testdata/simple2/main.go b/testdata/simple2/main.go index e506504a9..5858e02eb 100644 --- a/testdata/simple2/main.go +++ b/testdata/simple2/main.go @@ -1,7 +1,8 @@ package main import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/testdata/simple2/api" ) @@ -47,9 +48,8 @@ import ( // @authorizationurl https://example.com/oauth/authorize // @scope.admin Grants read and write access to administrative information func main() { - r := gin.New() - r.GET("/testapi/get-string-by-int/:some_id", api.GetStringByInt) - r.GET("//testapi/get-struct-array-by-string/:some_id", api.GetStructArrayByString) - r.POST("/testapi/upload", api.Upload) - r.Run() + 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/simple2/web/handler.go b/testdata/simple2/web/handler.go index d9a568692..bf5923d6c 100644 --- a/testdata/simple2/web/handler.go +++ b/testdata/simple2/web/handler.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - uuid "github.com/satori/go.uuid" + uuid "github.com/gofrs/uuid" "github.com/shopspring/decimal" ) @@ -53,8 +53,8 @@ type Pet struct { Hidden string `json:"-"` UUID uuid.UUID Decimal decimal.Decimal - customString CustomString - customStringArr []CustomString + CustomString CustomString + CustomStringArr []CustomString NullInt sql.NullInt64 `swaggertype:"integer"` Coeffs []big.Float `swaggertype:"array,number"` Birthday TimestampTime `swaggertype:"primitive,integer"` diff --git a/testdata/simple3/api/api.go b/testdata/simple3/api/api.go index bed956f38..10db26060 100644 --- a/testdata/simple3/api/api.go +++ b/testdata/simple3/api/api.go @@ -1,8 +1,6 @@ package api -import ( - "github.com/gin-gonic/gin" -) +import "net/http" // @Summary Add a new pet to the store // @Description get string by ID @@ -15,7 +13,7 @@ import ( // @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(c *gin.Context) { +func GetStringByInt(w http.ResponseWriter, r *http.Request) { //write your code } @@ -25,8 +23,8 @@ func GetStringByInt(c *gin.Context) { // @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" Mininum(0) default(0) -// @Param limit query int true "Limit" Maxinum(50) default(10) +// @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!!" @@ -38,7 +36,7 @@ func GetStringByInt(c *gin.Context) { // @Security OAuth2AccessCode[read] // @Security OAuth2Password[admin] // @Router /testapi/get-struct-array-by-string/{some_id} [get] -func GetStructArrayByString(c *gin.Context) { +func GetStructArrayByString(w http.ResponseWriter, r *http.Request) { //write your code } @@ -52,7 +50,7 @@ func GetStructArrayByString(c *gin.Context) { // @Failure 400 {object} web.APIError "We need ID!!" // @Failure 404 {object} web.APIError "Can not find ID" // @Router /file/upload [post] -func Upload(ctx *gin.Context) { +func Upload(w http.ResponseWriter, r *http.Request) { //write your code } diff --git a/testdata/simple3/main.go b/testdata/simple3/main.go index 38f8ef4a9..307cff1dd 100644 --- a/testdata/simple3/main.go +++ b/testdata/simple3/main.go @@ -1,7 +1,8 @@ package main import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/testdata/simple3/api" ) @@ -47,9 +48,8 @@ import ( // @authorizationurl https://example.com/oauth/authorize // @scope.admin Grants read and write access to administrative information func main() { - r := gin.New() - r.GET("/testapi/get-string-by-int/:some_id", api.GetStringByInt) - r.GET("//testapi/get-struct-array-by-string/:some_id", api.GetStructArrayByString) - r.POST("/testapi/upload", api.Upload) - r.Run() + 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/simple3/web/handler.go b/testdata/simple3/web/handler.go index 0447e4d26..505b7620b 100644 --- a/testdata/simple3/web/handler.go +++ b/testdata/simple3/web/handler.go @@ -3,7 +3,7 @@ package web import ( "time" - uuid "github.com/satori/go.uuid" + uuid "github.com/gofrs/uuid" "github.com/shopspring/decimal" ) diff --git a/testdata/simple_cgo/api/api.go b/testdata/simple_cgo/api/api.go new file mode 100644 index 000000000..d81ae1990 --- /dev/null +++ b/testdata/simple_cgo/api/api.go @@ -0,0 +1,17 @@ +package api + +import "net/http" + +// @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 int true "Some ID" +// @Success 200 {string} string "ok" +// @Failure 400 {object} string "We need ID!!" +// @Failure 404 {object} string "Can not find ID" +// @Router /testapi/get-string-by-int/{some_id} [get] +func GetStringByInt(w http.ResponseWriter, r *http.Request) { +} diff --git a/testdata/model_not_under_root/cmd/main.go b/testdata/simple_cgo/main.go similarity index 85% rename from testdata/model_not_under_root/cmd/main.go rename to testdata/simple_cgo/main.go index 03d7c3621..971ae70ed 100644 --- a/testdata/model_not_under_root/cmd/main.go +++ b/testdata/simple_cgo/main.go @@ -1,8 +1,18 @@ package main +/* +#include + +void Hello(){ + printf("Hello world\n"); +} +*/ +import "C" + import ( - "github.com/gin-gonic/gin" - "github.com/swaggo/swag/testdata/model_not_under_root/cmd/api" + "net/http" + + "github.com/swaggo/swag/testdata/simple_cgo/api" ) // @title Swagger Example API @@ -47,7 +57,8 @@ import ( // @authorizationurl https://example.com/oauth/authorize // @scope.admin Grants read and write access to administrative information func main() { - r := gin.New() - r.GET("/testapi/get-string-by-int/:some_id", api.GetStringByInt) - r.Run() + C.Hello() + + http.HandleFunc("/testapi/get-string-by-int/", api.GetStringByInt) + http.ListenAndServe(":8080", nil) } diff --git a/testdata/struct_comment/api/api.go b/testdata/struct_comment/api/api.go index 539f90923..f96433d2d 100644 --- a/testdata/struct_comment/api/api.go +++ b/testdata/struct_comment/api/api.go @@ -1,8 +1,6 @@ package api -import ( - "github.com/gin-gonic/gin" -) +import "net/http" // @Summary Add a new pet to the store // @Description get string by ID @@ -13,6 +11,6 @@ import ( // @Failure 400 {object} web.APIError "We need ID!!" // @Failure 404 {object} web.APIError "Can not find ID" // @Router /posts/{post_id} [get] -func GetPost(c *gin.Context) { +func GetPost(w http.ResponseWriter, r *http.Request) { //write your code } diff --git a/testdata/struct_comment/main.go b/testdata/struct_comment/main.go index 7fa1a2771..6994817b5 100644 --- a/testdata/struct_comment/main.go +++ b/testdata/struct_comment/main.go @@ -1,7 +1,8 @@ package main import ( - "github.com/gin-gonic/gin" + "net/http" + "github.com/swaggo/swag/testdata/struct_comment/api" ) @@ -11,7 +12,6 @@ import ( // @host localhost:4000 // @basePath /api func main() { - r := gin.New() - r.GET("/posts/:post_id", api.GetPost) - r.Run() + http.HandleFunc("/posts/", api.GetPost) + http.ListenAndServe(":8080", nil) } diff --git a/testdata/tags/cats.md b/testdata/tags/cats.md new file mode 100644 index 000000000..1fcfb09b0 --- /dev/null +++ b/testdata/tags/cats.md @@ -0,0 +1,3 @@ +## Cats + +Cats are also very cool! \ No newline at end of file diff --git a/testdata/tags/main.go b/testdata/tags/main.go index 381d545f9..d1507ebe0 100644 --- a/testdata/tags/main.go +++ b/testdata/tags/main.go @@ -8,5 +8,5 @@ package main // @tag.docs.url https://google.de // @tag.docs.description google is super useful to find out that cats are evil! // @tag.name apes -// @tag.description.markdown +// @tag.description Apes are also cool func main() {} diff --git a/testdata/tags2/apes.md b/testdata/tags2/apes.md new file mode 100644 index 000000000..5e6434d9f --- /dev/null +++ b/testdata/tags2/apes.md @@ -0,0 +1,3 @@ +## Apes + +Apes are very cool! \ No newline at end of file diff --git a/testdata/tags2/api.md b/testdata/tags2/api.md new file mode 100644 index 000000000..94c5bda83 --- /dev/null +++ b/testdata/tags2/api.md @@ -0,0 +1,5 @@ +## CoolApi Title + +### Cool API SubTitle + +We love markdown! \ No newline at end of file diff --git a/testdata/tags2/main.go b/testdata/tags2/main.go new file mode 100644 index 000000000..381d545f9 --- /dev/null +++ b/testdata/tags2/main.go @@ -0,0 +1,12 @@ +package main + +// @description.markdown +// @tag.name dogs +// @tag.description Dogs are cool +// @tag.name cats +// @tag.description Cats are the devil +// @tag.docs.url https://google.de +// @tag.docs.description google is super useful to find out that cats are evil! +// @tag.name apes +// @tag.description.markdown +func main() {} diff --git a/testdata/tags_nonexistend_tag/apes.md b/testdata/tags_nonexistend_tag/apes.md new file mode 100644 index 000000000..5e6434d9f --- /dev/null +++ b/testdata/tags_nonexistend_tag/apes.md @@ -0,0 +1,3 @@ +## Apes + +Apes are very cool! \ No newline at end of file diff --git a/testdata/tags_nonexistend_tag/api.md b/testdata/tags_nonexistend_tag/api.md new file mode 100644 index 000000000..94c5bda83 --- /dev/null +++ b/testdata/tags_nonexistend_tag/api.md @@ -0,0 +1,5 @@ +## CoolApi Title + +### Cool API SubTitle + +We love markdown! \ No newline at end of file diff --git a/testdata/tags_nonexistend_tag/main.go b/testdata/tags_nonexistend_tag/main.go new file mode 100644 index 000000000..5e8b6191b --- /dev/null +++ b/testdata/tags_nonexistend_tag/main.go @@ -0,0 +1,11 @@ +package main + +// @description.markdown +// @tag.name dogs +// @tag.description Dogs are cool +// @tag.name cats +// @tag.description Cats are the devil +// @tag.docs.url https://google.de +// @tag.docs.description google is super useful to find out that cats are evil! +// @tag.description.markdown +func main() {} diff --git a/types.go b/types.go new file mode 100644 index 000000000..cf849e2bf --- /dev/null +++ b/types.go @@ -0,0 +1,59 @@ +package swag + +import ( + "github.com/go-openapi/spec" + "go/ast" +) + +//Schema parsed schema +type Schema struct { + PkgPath string //package import path used to rename Name of a definition int case of conflict + Name string //Name in definitions + *spec.Schema // +} + +//TypeSpecDef the whole information of a typeSpec +type TypeSpecDef struct { + //path of package starting from under ${GOPATH}/src or from module path in go.mod + PkgPath string + + //ast file where TypeSpec is + File *ast.File + + //the TypeSpec of this type definition + TypeSpec *ast.TypeSpec +} + +//Name name of the typeSpec +func (t *TypeSpecDef) Name() string { + return t.TypeSpec.Name.Name +} + +//FullName full name of the typeSpec +func (t *TypeSpecDef) FullName() string { + return fullTypeName(t.File.Name.Name, t.TypeSpec.Name.Name) +} + +//AstFileInfo information of a ast.File +type AstFileInfo struct { + //File ast.File + File *ast.File + + //Path path of the ast.File + Path string + + //PackagePath package import path of the ast.File + PackagePath string +} + +//PackageDefinitions files and definition in a package +type PackageDefinitions struct { + //package name + Name string + + //files in this package, map key is file's relative path starting package path + Files map[string]*ast.File + + //definitions in this package, map key is typeName + TypeDefinitions map[string]*TypeSpecDef +} diff --git a/version.go b/version.go index 60c01d518..70c9b4ab9 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package swag // Version of swag -const Version = "v1.6.5" +const Version = "v1.7.0"