diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0446cbe7d..d984775be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,15 +10,15 @@ jobs: test: strategy: matrix: - go: [ '1.15.x', '1.16.x', '1.17.x', '1.18.x' ] + go: [ '1.16.x', '1.17.x', '1.18.x' ] platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@master + - uses: actions/checkout@v3 with: path: ./src/github.com/${{ github.repository }} - name: Set up Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: deps diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..a7e77a6cc --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,43 @@ +name: docker + +on: + push: + tags: + - 'v*' + +jobs: + docker-build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Github Packages + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/swaggo/swag + + - name: Build image and push to GitHub Container Registry + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: | + ghcr.io/swaggo/swag:latest + ghcr.io/swaggo/swag:${{github.ref_name}} + labels: ${{ steps.meta.outputs.labels }} + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.goreleaser.yml b/.goreleaser.yml index 6ce3665bf..222961768 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -10,6 +10,8 @@ build: ignore: - goos: darwin goarch: arm64 + env: + - CGO_ENABLED=0 archives: - replacements: diff --git a/Dockerfile b/Dockerfile index 65746bdde..170d0c699 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Dockerfile References: https://docs.docker.com/engine/reference/builder/ # Start from the latest golang base image -FROM golang:1.17-alpine as builder +FROM golang:1.18.3-alpine as builder # Set the Current Working Directory inside the container WORKDIR /app diff --git a/README.md b/README.md index 84239d117..44e699764 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ 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) + - [Function scoped struct declaration](#function-scoped-struct-declaration) - [Model composition in response](#model-composition-in-response) - [Add a headers in response](#add-a-headers-in-response) - [Use multiple path params](#use-multiple-path-params) @@ -51,12 +52,9 @@ Swag converts Go annotations to Swagger Documentation 2.0. We've created a varie 2. Download swag by using: ```sh -$ go get -u github.com/swaggo/swag/cmd/swag - -# 1.16 or newer $ go install github.com/swaggo/swag/cmd/swag@latest ``` -To build from source you need [Go](https://golang.org/dl/) (1.15 or newer). +To build from source you need [Go](https://golang.org/dl/) (1.16 or newer). Or download a pre-compiled binary from the [release page](https://github.com/swaggo/swag/releases). @@ -87,6 +85,7 @@ USAGE: swag init [command options] [arguments...] OPTIONS: + --quiet, -q Make the logger quiet. (default: false) --generalInfo value, -g value Go file path in which 'swagger general API Info' is written (default: "main.go") --dir value, -d value Directories you want to parse,comma separated and general-info file must be in the first one (default: "./") --exclude value Exclude directories and files when searching, comma separated @@ -100,8 +99,11 @@ OPTIONS: --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) + --requiredByDefault Set validation required for all fields by default (default: false) --instanceName value This parameter can be used to name different swagger document instances. It is optional. --overridesFile value File to read global type overrides from. (default: ".swaggo") + --parseGoList Parse dependency via 'go list' (default: true) + --tags value, -t value A comma-separated list of tags to filter the APIs for which the documentation is generated.Special case if the tag is prefixed with the '!' character then the APIs with that tag will be excluded --help, -h show help (default: false) ``` @@ -127,9 +129,12 @@ OPTIONS: - [echo](http://github.com/swaggo/echo-swagger) - [buffalo](https://github.com/swaggo/buffalo-swagger) - [net/http](https://github.com/swaggo/http-swagger) +- [gorilla/mux](https://github.com/swaggo/http-swagger) +- [go-chi/chi](https://github.com/swaggo/http-swagger) - [flamingo](https://github.com/i-love-flamingo/swagger) -- [fiber](https://github.com/arsmn/fiber-swagger) +- [fiber](https://github.com/gofiber/swagger) - [atreugo](https://github.com/Nerzal/atreugo-swagger) +- [hertz](https://github.com/hertz-contrib/swagger) ## How to use it with Gin @@ -488,7 +493,7 @@ type Foo struct { Field Name | Type | Description ---|:---:|--- -validate | `string` | Determines the validation for the parameter. Possible values are: `required`. +validate | `string` | Determines the validation for the parameter. Possible values are: `required,optional`. 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. @@ -537,6 +542,30 @@ type Account struct { } ``` + +### Function scoped struct declaration + +You can declare your request response structs inside a function body. +You must have to follow the naming convention `.. `. + +```go +package main + +// @Param request body main.MyHandler.request true "query params" +// @Success 200 {object} main.MyHandler.response +// @Router /test [post] +func MyHandler() { + type request struct { + RequestField string + } + + type response struct { + ResponseField string + } +} +``` + + ### Model composition in response ```go // JSONResult's data field will be overridden by the specific type proto.Order diff --git a/README_zh-CN.md b/README_zh-CN.md index 45a355f9e..173f865aa 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -47,13 +47,10 @@ Swag将Go的注释转换为Swagger2.0文档。我们为流行的 [Go Web Framewo 2. 使用如下命令下载swag: ```bash -$ go get -u github.com/swaggo/swag/cmd/swag - -# 1.16 及以上版本 $ go install github.com/swaggo/swag/cmd/swag@latest ``` -从源码开始构建的话,需要有Go环境(1.15及以上版本)。 +从源码开始构建的话,需要有Go环境(1.16及以上版本)。 或者从github的release页面下载预编译好的二进制文件。 @@ -123,6 +120,13 @@ OPTIONS: - [echo](http://github.com/swaggo/echo-swagger) - [buffalo](https://github.com/swaggo/buffalo-swagger) - [net/http](https://github.com/swaggo/http-swagger) +- [net/http](https://github.com/swaggo/http-swagger) +- [gorilla/mux](https://github.com/swaggo/http-swagger) +- [go-chi/chi](https://github.com/swaggo/http-swagger) +- [flamingo](https://github.com/i-love-flamingo/swagger) +- [fiber](https://github.com/gofiber/swagger) +- [atreugo](https://github.com/Nerzal/atreugo-swagger) +- [hertz](https://github.com/hertz-contrib/swagger) ## 如何与Gin集成 diff --git a/cmd/swag/main.go b/cmd/swag/main.go index f84a91afc..f755e3b09 100644 --- a/cmd/swag/main.go +++ b/cmd/swag/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "log" "os" "strings" @@ -15,24 +15,26 @@ import ( ) const ( - searchDirFlag = "dir" - excludeFlag = "exclude" - generalInfoFlag = "generalInfo" - propertyStrategyFlag = "propertyStrategy" - outputFlag = "output" - outputTypesFlag = "outputTypes" - parseVendorFlag = "parseVendor" - parseDependencyFlag = "parseDependency" - markdownFilesFlag = "markdownFiles" - codeExampleFilesFlag = "codeExampleFiles" - parseInternalFlag = "parseInternal" - generatedTimeFlag = "generatedTime" - parseDepthFlag = "parseDepth" - instanceNameFlag = "instanceName" - overridesFileFlag = "overridesFile" - parseGoListFlag = "parseGoList" - quietFlag = "quiet" - parseExtensionFlag = "parseExtension" + searchDirFlag = "dir" + excludeFlag = "exclude" + generalInfoFlag = "generalInfo" + propertyStrategyFlag = "propertyStrategy" + outputFlag = "output" + outputTypesFlag = "outputTypes" + parseVendorFlag = "parseVendor" + parseDependencyFlag = "parseDependency" + markdownFilesFlag = "markdownFiles" + codeExampleFilesFlag = "codeExampleFiles" + parseInternalFlag = "parseInternal" + generatedTimeFlag = "generatedTime" + requiredByDefaultFlag = "requiredByDefault" + parseDepthFlag = "parseDepth" + instanceNameFlag = "instanceName" + overridesFileFlag = "overridesFile" + parseGoListFlag = "parseGoList" + quietFlag = "quiet" + tagsFlag = "tags" + parseExtensionFlag = "parseExtension" ) var initFlags = []cli.Flag{ @@ -109,6 +111,10 @@ var initFlags = []cli.Flag{ Value: 100, Usage: "Dependency parse depth", }, + &cli.BoolFlag{ + Name: requiredByDefaultFlag, + Usage: "Set validation required for all fields by default", + }, &cli.StringFlag{ Name: instanceNameFlag, Value: "", @@ -128,6 +134,12 @@ var initFlags = []cli.Flag{ Name: parseExtensionFlag, Value: "", Usage: "Parse only those operations that match given extension", + }, + &cli.StringFlag{ + Name: tagsFlag, + Aliases: []string{"t"}, + Value: "", + Usage: "A comma-separated list of tags to filter the APIs for which the documentation is generated.Special case if the tag is prefixed with the '!' character then the APIs with that tag will be excluded", }, } @@ -144,9 +156,9 @@ func initAction(ctx *cli.Context) error { if len(outputTypes) == 0 { return fmt.Errorf("no output types specified") } - var logger swag.Debugger + logger := log.New(os.Stdout, "", log.LstdFlags) if ctx.Bool(quietFlag) { - logger = log.New(ioutil.Discard, "", log.LstdFlags) + logger = log.New(io.Discard, "", log.LstdFlags) } return gen.New().Build(&gen.Config{ @@ -162,11 +174,13 @@ func initAction(ctx *cli.Context) error { MarkdownFilesDir: ctx.String(markdownFilesFlag), ParseInternal: ctx.Bool(parseInternalFlag), GeneratedTime: ctx.Bool(generatedTimeFlag), + RequiredByDefault: ctx.Bool(requiredByDefaultFlag), CodeExampleFilesDir: ctx.String(codeExampleFilesFlag), ParseDepth: ctx.Int(parseDepthFlag), InstanceName: ctx.String(instanceNameFlag), OverridesFile: ctx.String(overridesFileFlag), ParseGoList: ctx.Bool(parseGoListFlag), + Tags: ctx.String(tagsFlag), Debugger: logger, }) } diff --git a/const.go b/const.go new file mode 100644 index 000000000..1353b6d1d --- /dev/null +++ b/const.go @@ -0,0 +1,566 @@ +package swag + +import ( + "go/ast" + "go/token" + "reflect" + "strconv" + "strings" +) + +// ConstVariable a model to record a const variable +type ConstVariable struct { + Name *ast.Ident + Type ast.Expr + Value interface{} + Comment *ast.CommentGroup + File *ast.File + Pkg *PackageDefinitions +} + +var escapedChars = map[uint8]uint8{ + 'n': '\n', + 'r': '\r', + 't': '\t', + 'v': '\v', + '\\': '\\', + '"': '"', +} + +// EvaluateEscapedChar parse escaped character +func EvaluateEscapedChar(text string) rune { + if len(text) == 1 { + return rune(text[0]) + } + + if len(text) == 2 && text[0] == '\\' { + return rune(escapedChars[text[1]]) + } + + if len(text) == 6 && text[0:2] == "\\u" { + n, err := strconv.ParseInt(text[2:], 16, 32) + if err == nil { + return rune(n) + } + } + + return 0 +} + +// EvaluateEscapedString parse escaped characters in string +func EvaluateEscapedString(text string) string { + if !strings.ContainsRune(text, '\\') { + return text + } + result := make([]byte, 0, len(text)) + for i := 0; i < len(text); i++ { + if text[i] == '\\' { + i++ + if text[i] == 'u' { + i++ + char, err := strconv.ParseInt(text[i:i+4], 16, 32) + if err == nil { + result = AppendUtf8Rune(result, rune(char)) + } + i += 3 + } else if c, ok := escapedChars[text[i]]; ok { + result = append(result, c) + } + } else { + result = append(result, text[i]) + } + } + return string(result) +} + +// EvaluateDataConversion evaluate the type a explicit type conversion +func EvaluateDataConversion(x interface{}, typeName string) interface{} { + switch value := x.(type) { + case int: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case uint: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case int8: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case uint8: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case int16: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case uint16: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case int32: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + case "string": + return string(value) + } + case uint32: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case int64: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case uint64: + switch typeName { + case "int": + return int(value) + case "byte": + return byte(value) + case "int8": + return int8(value) + case "int16": + return int16(value) + case "int32": + return int32(value) + case "int64": + return int64(value) + case "uint": + return uint(value) + case "uint8": + return uint8(value) + case "uint16": + return uint16(value) + case "uint32": + return uint32(value) + case "uint64": + return uint64(value) + case "rune": + return rune(value) + } + case string: + switch typeName { + case "string": + return value + } + } + return nil +} + +// EvaluateUnary evaluate the type and value of a unary expression +func EvaluateUnary(x interface{}, operator token.Token, xtype ast.Expr) (interface{}, ast.Expr) { + switch operator { + case token.SUB: + switch value := x.(type) { + case int: + return -value, xtype + case int8: + return -value, xtype + case int16: + return -value, xtype + case int32: + return -value, xtype + case int64: + return -value, xtype + } + case token.XOR: + switch value := x.(type) { + case int: + return ^value, xtype + case int8: + return ^value, xtype + case int16: + return ^value, xtype + case int32: + return ^value, xtype + case int64: + return ^value, xtype + case uint: + return ^value, xtype + case uint8: + return ^value, xtype + case uint16: + return ^value, xtype + case uint32: + return ^value, xtype + case uint64: + return ^value, xtype + } + } + return nil, nil +} + +// EvaluateBinary evaluate the type and value of a binary expression +func EvaluateBinary(x, y interface{}, operator token.Token, xtype, ytype ast.Expr) (interface{}, ast.Expr) { + if operator == token.SHR || operator == token.SHL { + var rightOperand uint64 + yValue := CanIntegerValue{reflect.ValueOf(y)} + if yValue.CanUint() { + rightOperand = yValue.Uint() + } else if yValue.CanInt() { + rightOperand = uint64(yValue.Int()) + } + + switch operator { + case token.SHL: + switch xValue := x.(type) { + case int: + return xValue << rightOperand, xtype + case int8: + return xValue << rightOperand, xtype + case int16: + return xValue << rightOperand, xtype + case int32: + return xValue << rightOperand, xtype + case int64: + return xValue << rightOperand, xtype + case uint: + return xValue << rightOperand, xtype + case uint8: + return xValue << rightOperand, xtype + case uint16: + return xValue << rightOperand, xtype + case uint32: + return xValue << rightOperand, xtype + case uint64: + return xValue << rightOperand, xtype + } + case token.SHR: + switch xValue := x.(type) { + case int: + return xValue >> rightOperand, xtype + case int8: + return xValue >> rightOperand, xtype + case int16: + return xValue >> rightOperand, xtype + case int32: + return xValue >> rightOperand, xtype + case int64: + return xValue >> rightOperand, xtype + case uint: + return xValue >> rightOperand, xtype + case uint8: + return xValue >> rightOperand, xtype + case uint16: + return xValue >> rightOperand, xtype + case uint32: + return xValue >> rightOperand, xtype + case uint64: + return xValue >> rightOperand, xtype + } + } + return nil, nil + } + + evalType := xtype + if evalType == nil { + evalType = ytype + } + + xValue := CanIntegerValue{reflect.ValueOf(x)} + yValue := CanIntegerValue{reflect.ValueOf(y)} + if xValue.Kind() == reflect.String && yValue.Kind() == reflect.String { + return xValue.String() + yValue.String(), evalType + } + + var targetValue reflect.Value + if xValue.Kind() != reflect.Int { + targetValue = reflect.New(xValue.Type()).Elem() + } else { + targetValue = reflect.New(yValue.Type()).Elem() + } + + switch operator { + case token.ADD: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() + yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() + yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) + yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() + uint64(yValue.Int())) + } + case token.SUB: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() - yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() - yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) - yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() - uint64(yValue.Int())) + } + case token.MUL: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() * yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() * yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) * yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() * uint64(yValue.Int())) + } + case token.QUO: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() / yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() / yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) / yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() / uint64(yValue.Int())) + } + case token.REM: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() % yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() % yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) % yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() % uint64(yValue.Int())) + } + case token.AND: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() & yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() & yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) & yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() & uint64(yValue.Int())) + } + case token.OR: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() | yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() | yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) | yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() | uint64(yValue.Int())) + } + case token.XOR: + if xValue.CanInt() && yValue.CanInt() { + targetValue.SetInt(xValue.Int() ^ yValue.Int()) + } else if xValue.CanUint() && yValue.CanUint() { + targetValue.SetUint(xValue.Uint() ^ yValue.Uint()) + } else if xValue.CanInt() && yValue.CanUint() { + targetValue.SetUint(uint64(xValue.Int()) ^ yValue.Uint()) + } else if xValue.CanUint() && yValue.CanInt() { + targetValue.SetUint(xValue.Uint() ^ uint64(yValue.Int())) + } + } + return targetValue.Interface(), evalType +} diff --git a/enums.go b/enums.go new file mode 100644 index 000000000..38658f20a --- /dev/null +++ b/enums.go @@ -0,0 +1,13 @@ +package swag + +const ( + enumVarNamesExtension = "x-enum-varnames" + enumCommentsExtension = "x-enum-comments" +) + +// EnumValue a model to record an enum consts variable +type EnumValue struct { + key string + Value interface{} + Comment string +} diff --git a/enums_test.go b/enums_test.go new file mode 100644 index 000000000..dbae10d83 --- /dev/null +++ b/enums_test.go @@ -0,0 +1,32 @@ +package swag + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseGlobalEnums(t *testing.T) { + searchDir := "testdata/enums" + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) + assert.NoError(t, err) + + p := New() + err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + b, err := json.MarshalIndent(p.swagger, "", " ") + assert.NoError(t, err) + assert.Equal(t, string(expected), string(b)) + constsPath := "github.com/swaggo/swag/testdata/enums/consts" + assert.Equal(t, 64, p.packages.packages[constsPath].ConstTable["uintSize"].Value) + assert.Equal(t, int32(62), p.packages.packages[constsPath].ConstTable["maxBase"].Value) + assert.Equal(t, 8, p.packages.packages[constsPath].ConstTable["shlByLen"].Value) + assert.Equal(t, 255, p.packages.packages[constsPath].ConstTable["hexnum"].Value) + assert.Equal(t, 15, p.packages.packages[constsPath].ConstTable["octnum"].Value) + assert.Equal(t, `aa\nbb\u8888cc`, p.packages.packages[constsPath].ConstTable["nonescapestr"].Value) + assert.Equal(t, "aa\nbb\u8888cc", p.packages.packages[constsPath].ConstTable["escapestr"].Value) + assert.Equal(t, '\u8888', p.packages.packages[constsPath].ConstTable["escapechar"].Value) +} diff --git a/example/basic/api/api.go b/example/basic/api/api.go index 52be29cd5..ed9e042f3 100644 --- a/example/basic/api/api.go +++ b/example/basic/api/api.go @@ -8,17 +8,18 @@ import ( ) // GetStringByInt example -// @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" -// @Param some_id body web.Pet true "Some ID" -// @Success 200 {string} string "ok" -// @Failure 400 {object} web.APIError "We need ID!!" -// @Failure 404 {object} web.APIError "Can not find ID" -// @Router /testapi/get-string-by-int/{some_id} [get] +// +// @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" +// @Param some_id body web.Pet true "Some ID" +// @Success 200 {string} string "ok" +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 404 {object} web.APIError "Can not find ID" +// @Router /testapi/get-string-by-int/{some_id} [get] func GetStringByInt(w http.ResponseWriter, r *http.Request) { var pet web.Pet if err := json.NewDecoder(r.Body).Decode(&pet); err != nil { @@ -30,39 +31,42 @@ func GetStringByInt(w http.ResponseWriter, r *http.Request) { } // GetStructArrayByString example -// @Description get struct array by ID -// @ID get-struct-array-by-string -// @Accept json -// @Produce json -// @Param some_id path string true "Some ID" -// @Param offset query int true "Offset" -// @Param limit query int true "Offset" -// @Success 200 {string} string "ok" -// @Failure 400 {object} web.APIError "We need ID!!" -// @Failure 404 {object} web.APIError "Can not find ID" -// @Router /testapi/get-struct-array-by-string/{some_id} [get] +// +// @Description get struct array by ID +// @ID get-struct-array-by-string +// @Accept json +// @Produce json +// @Param some_id path string true "Some ID" +// @Param offset query int true "Offset" +// @Param limit query int true "Offset" +// @Success 200 {string} string "ok" +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 404 {object} web.APIError "Can not find ID" +// @Router /testapi/get-struct-array-by-string/{some_id} [get] func GetStructArrayByString(w http.ResponseWriter, r *http.Request) { // write your code } // Upload example -// @Summary Upload file -// @Description Upload file -// @ID file.upload -// @Accept multipart/form-data -// @Produce json -// @Param file formData file true "this is a test file" -// @Success 200 {string} string "ok" -// @Failure 400 {object} web.APIError "We need ID!!" -// @Failure 404 {object} web.APIError "Can not find ID" -// @Router /file/upload [post] +// +// @Summary Upload file +// @Description Upload file +// @ID file.upload +// @Accept multipart/form-data +// @Produce json +// @Param file formData file true "this is a test file" +// @Success 200 {string} string "ok" +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 404 {object} web.APIError "Can not find ID" +// @Router /file/upload [post] func Upload(w http.ResponseWriter, r *http.Request) { // write your code } // AnonymousField example -// @Summary use Anonymous field -// @Success 200 {object} web.RevValue "ok" +// +// @Summary use Anonymous field +// @Success 200 {object} web.RevValue "ok" func AnonymousField() { } diff --git a/example/basic/main.go b/example/basic/main.go index 9f22eb586..ada8112ac 100644 --- a/example/basic/main.go +++ b/example/basic/main.go @@ -6,20 +6,21 @@ import ( "github.com/swaggo/swag/example/basic/api" ) -// @title Swagger Example API -// @version 1.0 -// @description This is a sample server Petstore server. -// @termsOfService http://swagger.io/terms/ +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @termsOfService http://swagger.io/terms/ -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host petstore.swagger.io +// @BasePath /v2 -// @host petstore.swagger.io -// @BasePath /v2 func main() { http.HandleFunc("/testapi/get-string-by-int/", api.GetStringByInt) http.HandleFunc("//testapi/get-struct-array-by-string/", api.GetStructArrayByString) diff --git a/example/celler/controller/accounts.go b/example/celler/controller/accounts.go index e52ec81ec..2815e0207 100644 --- a/example/celler/controller/accounts.go +++ b/example/celler/controller/accounts.go @@ -11,17 +11,18 @@ import ( ) // ShowAccount godoc -// @Summary Show an account -// @Description get string by ID -// @Tags accounts -// @Accept json -// @Produce json -// @Param id path int true "Account ID" -// @Success 200 {object} model.Account -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /accounts/{id} [get] +// +// @Summary Show an account +// @Description get string by ID +// @Tags accounts +// @Accept json +// @Produce json +// @Param id path int true "Account ID" +// @Success 200 {object} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts/{id} [get] func (c *Controller) ShowAccount(ctx *gin.Context) { id := ctx.Param("id") aid, err := strconv.Atoi(id) @@ -38,17 +39,18 @@ func (c *Controller) ShowAccount(ctx *gin.Context) { } // ListAccounts godoc -// @Summary List accounts -// @Description get accounts -// @Tags accounts -// @Accept json -// @Produce json -// @Param q query string false "name search by q" Format(email) -// @Success 200 {array} model.Account -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /accounts [get] +// +// @Summary List accounts +// @Description get accounts +// @Tags accounts +// @Accept json +// @Produce json +// @Param q query string false "name search by q" Format(email) +// @Success 200 {array} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts [get] func (c *Controller) ListAccounts(ctx *gin.Context) { q := ctx.Request.URL.Query().Get("q") accounts, err := model.AccountsAll(q) @@ -60,17 +62,18 @@ func (c *Controller) ListAccounts(ctx *gin.Context) { } // AddAccount godoc -// @Summary Add an account -// @Description add by json account -// @Tags accounts -// @Accept json -// @Produce json -// @Param account body model.AddAccount true "Add account" -// @Success 200 {object} model.Account -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /accounts [post] +// +// @Summary Add an account +// @Description add by json account +// @Tags accounts +// @Accept json +// @Produce json +// @Param account body model.AddAccount true "Add account" +// @Success 200 {object} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts [post] func (c *Controller) AddAccount(ctx *gin.Context) { var addAccount model.AddAccount if err := ctx.ShouldBindJSON(&addAccount); err != nil { @@ -94,18 +97,19 @@ func (c *Controller) AddAccount(ctx *gin.Context) { } // UpdateAccount godoc -// @Summary Update an account -// @Description Update by json account -// @Tags accounts -// @Accept json -// @Produce json -// @Param id path int true "Account ID" -// @Param account body model.UpdateAccount true "Update account" -// @Success 200 {object} model.Account -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /accounts/{id} [patch] +// +// @Summary Update an account +// @Description Update by json account +// @Tags accounts +// @Accept json +// @Produce json +// @Param id path int true "Account ID" +// @Param account body model.UpdateAccount true "Update account" +// @Success 200 {object} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts/{id} [patch] func (c *Controller) UpdateAccount(ctx *gin.Context) { id := ctx.Param("id") aid, err := strconv.Atoi(id) @@ -131,17 +135,18 @@ func (c *Controller) UpdateAccount(ctx *gin.Context) { } // DeleteAccount godoc -// @Summary Delete an account -// @Description Delete by account ID -// @Tags accounts -// @Accept json -// @Produce json -// @Param id path int true "Account ID" Format(int64) -// @Success 204 {object} model.Account -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /accounts/{id} [delete] +// +// @Summary Delete an account +// @Description Delete by account ID +// @Tags accounts +// @Accept json +// @Produce json +// @Param id path int true "Account ID" Format(int64) +// @Success 204 {object} model.Account +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts/{id} [delete] func (c *Controller) DeleteAccount(ctx *gin.Context) { id := ctx.Param("id") aid, err := strconv.Atoi(id) @@ -158,18 +163,19 @@ func (c *Controller) DeleteAccount(ctx *gin.Context) { } // UploadAccountImage godoc -// @Summary Upload account image -// @Description Upload file -// @Tags accounts -// @Accept multipart/form-data -// @Produce json -// @Param id path int true "Account ID" -// @Param file formData file true "account image" -// @Success 200 {object} controller.Message -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /accounts/{id}/images [post] +// +// @Summary Upload account image +// @Description Upload file +// @Tags accounts +// @Accept multipart/form-data +// @Produce json +// @Param id path int true "Account ID" +// @Param file formData file true "account image" +// @Success 200 {object} controller.Message +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /accounts/{id}/images [post] func (c *Controller) UploadAccountImage(ctx *gin.Context) { id, err := strconv.Atoi(ctx.Param("id")) if err != nil { diff --git a/example/celler/controller/admin.go b/example/celler/controller/admin.go index 65f16b2b7..b288d3484 100644 --- a/example/celler/controller/admin.go +++ b/example/celler/controller/admin.go @@ -11,18 +11,19 @@ import ( ) // Auth godoc -// @Summary Auth admin -// @Description get admin info -// @Tags accounts,admin -// @Accept json -// @Produce json -// @Success 200 {object} model.Admin -// @Failure 400 {object} httputil.HTTPError -// @Failure 401 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Security ApiKeyAuth -// @Router /admin/auth [post] +// +// @Summary Auth admin +// @Description get admin info +// @Tags accounts,admin +// @Accept json +// @Produce json +// @Success 200 {object} model.Admin +// @Failure 400 {object} httputil.HTTPError +// @Failure 401 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Security ApiKeyAuth +// @Router /admin/auth [post] func (c *Controller) Auth(ctx *gin.Context) { authHeader := ctx.GetHeader("Authorization") if len(authHeader) == 0 { diff --git a/example/celler/controller/bottles.go b/example/celler/controller/bottles.go index 4f35ee5cf..925414d32 100644 --- a/example/celler/controller/bottles.go +++ b/example/celler/controller/bottles.go @@ -11,18 +11,19 @@ import ( ) // ShowBottle godoc -// @Summary Show a bottle -// @Description get string by ID -// @ID get-string-by-int -// @Tags bottles -// @Accept json -// @Produce json -// @Param id path int true "Bottle ID" -// @Success 200 {object} model.Bottle -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /bottles/{id} [get] +// +// @Summary Show a bottle +// @Description get string by ID +// @ID get-string-by-int +// @Tags bottles +// @Accept json +// @Produce json +// @Param id path int true "Bottle ID" +// @Success 200 {object} model.Bottle +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /bottles/{id} [get] func (c *Controller) ShowBottle(ctx *gin.Context) { id := ctx.Param("id") bid, err := strconv.Atoi(id) @@ -39,16 +40,17 @@ func (c *Controller) ShowBottle(ctx *gin.Context) { } // ListBottles godoc -// @Summary List bottles -// @Description get bottles -// @Tags bottles -// @Accept json -// @Produce json -// @Success 200 {array} model.Bottle -// @Failure 400 {object} httputil.HTTPError -// @Failure 404 {object} httputil.HTTPError -// @Failure 500 {object} httputil.HTTPError -// @Router /bottles [get] +// +// @Summary List bottles +// @Description get bottles +// @Tags bottles +// @Accept json +// @Produce json +// @Success 200 {array} model.Bottle +// @Failure 400 {object} httputil.HTTPError +// @Failure 404 {object} httputil.HTTPError +// @Failure 500 {object} httputil.HTTPError +// @Router /bottles [get] func (c *Controller) ListBottles(ctx *gin.Context) { bottles, err := model.BottlesAll() if err != nil { diff --git a/example/celler/controller/examples.go b/example/celler/controller/examples.go index 8118d7f0d..e27a4c3d5 100644 --- a/example/celler/controller/examples.go +++ b/example/celler/controller/examples.go @@ -10,33 +10,35 @@ import ( ) // PingExample godoc -// @Summary ping example -// @Description do ping -// @Tags example -// @Accept json -// @Produce plain -// @Success 200 {string} string "pong" -// @Failure 400 {string} string "ok" -// @Failure 404 {string} string "ok" -// @Failure 500 {string} string "ok" -// @Router /examples/ping [get] +// +// @Summary ping example +// @Description do ping +// @Tags example +// @Accept json +// @Produce plain +// @Success 200 {string} string "pong" +// @Failure 400 {string} string "ok" +// @Failure 404 {string} string "ok" +// @Failure 500 {string} string "ok" +// @Router /examples/ping [get] func (c *Controller) PingExample(ctx *gin.Context) { ctx.String(http.StatusOK, "pong") } // CalcExample godoc -// @Summary calc example -// @Description plus -// @Tags example -// @Accept json -// @Produce plain -// @Param val1 query int true "used for calc" -// @Param val2 query int true "used for calc" -// @Success 200 {integer} string "answer" -// @Failure 400 {string} string "ok" -// @Failure 404 {string} string "ok" -// @Failure 500 {string} string "ok" -// @Router /examples/calc [get] +// +// @Summary calc example +// @Description plus +// @Tags example +// @Accept json +// @Produce plain +// @Param val1 query int true "used for calc" +// @Param val2 query int true "used for calc" +// @Success 200 {integer} string "answer" +// @Failure 400 {string} string "ok" +// @Failure 404 {string} string "ok" +// @Failure 500 {string} string "ok" +// @Router /examples/calc [get] func (c *Controller) CalcExample(ctx *gin.Context) { val1, err := strconv.Atoi(ctx.Query("val1")) if err != nil { @@ -53,18 +55,19 @@ func (c *Controller) CalcExample(ctx *gin.Context) { } // PathParamsExample godoc -// @Summary path params example -// @Description path params -// @Tags example -// @Accept json -// @Produce plain -// @Param group_id path int true "Group ID" -// @Param account_id path int true "Account ID" -// @Success 200 {string} string "answer" -// @Failure 400 {string} string "ok" -// @Failure 404 {string} string "ok" -// @Failure 500 {string} string "ok" -// @Router /examples/groups/{group_id}/accounts/{account_id} [get] +// +// @Summary path params example +// @Description path params +// @Tags example +// @Accept json +// @Produce plain +// @Param group_id path int true "Group ID" +// @Param account_id path int true "Account ID" +// @Success 200 {string} string "answer" +// @Failure 400 {string} string "ok" +// @Failure 404 {string} string "ok" +// @Failure 500 {string} string "ok" +// @Router /examples/groups/{group_id}/accounts/{account_id} [get] func (c *Controller) PathParamsExample(ctx *gin.Context) { groupID, err := strconv.Atoi(ctx.Param("group_id")) if err != nil { @@ -80,55 +83,58 @@ func (c *Controller) PathParamsExample(ctx *gin.Context) { } // HeaderExample godoc -// @Summary custome header example -// @Description custome header -// @Tags example -// @Accept json -// @Produce plain -// @Param Authorization header string true "Authentication header" -// @Success 200 {string} string "answer" -// @Failure 400 {string} string "ok" -// @Failure 404 {string} string "ok" -// @Failure 500 {string} string "ok" -// @Router /examples/header [get] +// +// @Summary custome header example +// @Description custome header +// @Tags example +// @Accept json +// @Produce plain +// @Param Authorization header string true "Authentication header" +// @Success 200 {string} string "answer" +// @Failure 400 {string} string "ok" +// @Failure 404 {string} string "ok" +// @Failure 500 {string} string "ok" +// @Router /examples/header [get] func (c *Controller) HeaderExample(ctx *gin.Context) { ctx.String(http.StatusOK, ctx.GetHeader("Authorization")) } // SecuritiesExample godoc -// @Summary custome header example -// @Description custome header -// @Tags example -// @Accept json -// @Produce json -// @Param Authorization header string true "Authentication header" -// @Success 200 {string} string "answer" -// @Failure 400 {string} string "ok" -// @Failure 404 {string} string "ok" -// @Failure 500 {string} string "ok" -// @Security ApiKeyAuth -// @Security OAuth2Implicit[admin, write] -// @Router /examples/securities [get] +// +// @Summary custome header example +// @Description custome header +// @Tags example +// @Accept json +// @Produce json +// @Param Authorization header string true "Authentication header" +// @Success 200 {string} string "answer" +// @Failure 400 {string} string "ok" +// @Failure 404 {string} string "ok" +// @Failure 500 {string} string "ok" +// @Security ApiKeyAuth +// @Security OAuth2Implicit[admin, write] +// @Router /examples/securities [get] func (c *Controller) SecuritiesExample(ctx *gin.Context) { } // AttributeExample godoc -// @Summary attribute example -// @Description attribute -// @Tags example -// @Accept json -// @Produce plain -// @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) -// @Success 200 {string} string "answer" -// @Failure 400 {string} string "ok" -// @Failure 404 {string} string "ok" -// @Failure 500 {string} string "ok" -// @Router /examples/attribute [get] +// +// @Summary attribute example +// @Description attribute +// @Tags example +// @Accept json +// @Produce plain +// @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) +// @Success 200 {string} string "answer" +// @Failure 400 {string} string "ok" +// @Failure 404 {string} string "ok" +// @Failure 500 {string} string "ok" +// @Router /examples/attribute [get] func (c *Controller) AttributeExample(ctx *gin.Context) { ctx.String(http.StatusOK, fmt.Sprintf("enumstring=%s enumint=%s enumnumber=%s string=%s int=%s default=%s", ctx.Query("enumstring"), @@ -141,13 +147,14 @@ func (c *Controller) AttributeExample(ctx *gin.Context) { } // 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] +// +// @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/main.go b/example/celler/main.go index 87d4742a4..ec27d4757 100644 --- a/example/celler/main.go +++ b/example/celler/main.go @@ -13,48 +13,48 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) -// @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 - -// @securityDefinitions.basic BasicAuth - -// @securityDefinitions.apikey ApiKeyAuth -// @in header -// @name Authorization -// @description Description for what is this security definition being used - -// @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 +// @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 + +// @securityDefinitions.basic BasicAuth + +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name Authorization +// @description Description for what is this security definition being used + +// @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 func main() { r := gin.Default() diff --git a/example/celler/model/account.go b/example/celler/model/account.go index 54fb9b5d6..d921ef1ce 100644 --- a/example/celler/model/account.go +++ b/example/celler/model/account.go @@ -14,7 +14,7 @@ type Account struct { UUID uuid.UUID `json:"uuid" example:"550e8400-e29b-41d4-a716-446655440000" format:"uuid"` } -// example +// example var ( ErrNameInvalid = errors.New("name is empty") ) diff --git a/example/go-module-support/main.go b/example/go-module-support/main.go index 40d29d749..b576166ef 100644 --- a/example/go-module-support/main.go +++ b/example/go-module-support/main.go @@ -5,20 +5,21 @@ import ( "github.com/swaggo/examples/go-module-support/api" // included package from external ) -// @title Swagger Example API -// @version 1.0 -// @description This is a sample server Petstore server. -// @termsOfService http://swagger.io/terms/ +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @termsOfService http://swagger.io/terms/ -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host petstore.swagger.io +// @BasePath /v2 -// @host petstore.swagger.io -// @BasePath /v2 func main() { r := gin.New() r.GET("/testapi/get-string-by-int/:some_id", api.GetStringByInt) diff --git a/example/markdown/api/api.go b/example/markdown/api/api.go index 578271da8..2597b2886 100644 --- a/example/markdown/api/api.go +++ b/example/markdown/api/api.go @@ -23,54 +23,58 @@ type APIError struct { } // ListUsers example -// @Summary List users from the store -// @Tags admin -// @Accept json -// @Produce json -// @Success 200 {array} api.UsersCollection "ok" -// @Router /admin/user/ [get] +// +// @Summary List users from the store +// @Tags admin +// @Accept json +// @Produce json +// @Success 200 {array} api.UsersCollection "ok" +// @Router /admin/user/ [get] func ListUsers(w http.ResponseWriter, r *http.Request) { // write your code } // GetUser example -// @Summary Read user from the store -// @Tags admin -// @Accept json -// @Produce json -// @Param id path int true "User Id" -// @Success 200 {object} api.User -// @Failure 400 {object} api.APIError "We need ID!!" -// @Failure 404 {object} api.APIError "Can not find ID" -// @Router /admin/user/{id} [get] +// +// @Summary Read user from the store +// @Tags admin +// @Accept json +// @Produce json +// @Param id path int true "User Id" +// @Success 200 {object} api.User +// @Failure 400 {object} api.APIError "We need ID!!" +// @Failure 404 {object} api.APIError "Can not find ID" +// @Router /admin/user/{id} [get] func GetUser(w http.ResponseWriter, r *http.Request) { // write your code } // AddUser example -// @Summary Add a new user to the store -// @Tags admin -// @Accept json -// @Produce json -// @Param message body api.User true "User Data" -// @Success 200 {string} string "ok" -// @Failure 400 {object} api.APIError "We need ID!!" -// @Failure 404 {object} api.APIError "Can not find ID" -// @Router /admin/user/ [post] +// +// @Summary Add a new user to the store +// @Tags admin +// @Accept json +// @Produce json +// @Param message body api.User true "User Data" +// @Success 200 {string} string "ok" +// @Failure 400 {object} api.APIError "We need ID!!" +// @Failure 404 {object} api.APIError "Can not find ID" +// @Router /admin/user/ [post] func AddUser(w http.ResponseWriter, r *http.Request) { // write your code } // UpdateUser example -// @Summary Add a new user to the store -// @Tags admin -// @Accept json -// @Produce json -// @Param message body api.User true "User Data" -// @Success 200 {string} string "ok" -// @Failure 400 {object} api.APIError "We need ID!!" -// @Failure 404 {object} api.APIError "Can not find ID" -// @Router /admin/user/ [put] +// +// @Summary Add a new user to the store +// @Tags admin +// @Accept json +// @Produce json +// @Param message body api.User true "User Data" +// @Success 200 {string} string "ok" +// @Failure 400 {object} api.APIError "We need ID!!" +// @Failure 404 {object} api.APIError "Can not find ID" +// @Router /admin/user/ [put] func UpdateUser(w http.ResponseWriter, r *http.Request) { // write your code } diff --git a/example/markdown/main.go b/example/markdown/main.go index bd2b97ae2..a13720c27 100644 --- a/example/markdown/main.go +++ b/example/markdown/main.go @@ -9,23 +9,23 @@ import ( _ "github.com/swaggo/swag/example/markdown/docs" ) -// @title Swagger Example API -// @version 1.0 -// @description This is a sample server Petstore server. -// @description.markdown -// @termsOfService http://swagger.io/terms/ +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @description.markdown +// @termsOfService http://swagger.io/terms/ -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @tag.name admin -// @tag.description.markdown +// @tag.name admin +// @tag.description.markdown -// @BasePath /v2 +// @BasePath /v2 func main() { router := mux.NewRouter() diff --git a/example/object-map-example/controller/api.go b/example/object-map-example/controller/api.go index 83b5e99fe..ecc4daecd 100644 --- a/example/object-map-example/controller/api.go +++ b/example/object-map-example/controller/api.go @@ -3,13 +3,14 @@ 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] +// +// @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{ diff --git a/example/object-map-example/main.go b/example/object-map-example/main.go index 0108f2c9f..7ab380892 100644 --- a/example/object-map-example/main.go +++ b/example/object-map-example/main.go @@ -9,15 +9,16 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) -// @title Swagger Map Example API -// @version 1.0 -// @termsOfService http://swagger.io/terms/ +// @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 +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host localhost:8080 +// @BasePath /api/v1 -// @host localhost:8080 -// @BasePath /api/v1 func main() { r := gin.Default() diff --git a/example/override/.swaggo b/example/override/.swaggo new file mode 100644 index 000000000..07fc329cf --- /dev/null +++ b/example/override/.swaggo @@ -0,0 +1,2 @@ +replace sql.NullString string +replace sql.NullInt64 int64 diff --git a/example/override/docs/docs.go b/example/override/docs/docs.go new file mode 100644 index 000000000..7b849a658 --- /dev/null +++ b/example/override/docs/docs.go @@ -0,0 +1,89 @@ +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "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": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/testapi/update-product/{product_id}": { + "post": { + "consumes": [ + "application/json" + ], + "summary": "Update product attributes", + "operationId": "update-product", + "parameters": [ + { + "type": "integer", + "description": "Product ID", + "name": "product_id", + "in": "path", + "required": true + }, + { + "description": " ", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.ProductUpdates" + } + } + ], + "responses": {} + } + } + }, + "definitions": { + "main.ProductUpdates": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "stock": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "product_info.swagger.io", + BasePath: "/v2", + Schemes: []string{}, + Title: "Swagger Example API", + Description: "This is a sample server for updating product information.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/example/override/docs/swagger.json b/example/override/docs/swagger.json new file mode 100644 index 000000000..479b774b4 --- /dev/null +++ b/example/override/docs/swagger.json @@ -0,0 +1,66 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server for updating product information.", + "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": "product_info.swagger.io", + "basePath": "/v2", + "paths": { + "/testapi/update-product/{product_id}": { + "post": { + "consumes": [ + "application/json" + ], + "summary": "Update product attributes", + "operationId": "update-product", + "parameters": [ + { + "type": "integer", + "description": "Product ID", + "name": "product_id", + "in": "path", + "required": true + }, + { + "description": " ", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.ProductUpdates" + } + } + ], + "responses": {} + } + } + }, + "definitions": { + "main.ProductUpdates": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "stock": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/example/override/docs/swagger.yaml b/example/override/docs/swagger.yaml new file mode 100644 index 000000000..091007419 --- /dev/null +++ b/example/override/docs/swagger.yaml @@ -0,0 +1,45 @@ +basePath: /v2 +definitions: + main.ProductUpdates: + properties: + description: + type: string + stock: + type: integer + type: + type: string + type: object +host: product_info.swagger.io +info: + contact: + email: support@swagger.io + name: API Support + url: http://www.swagger.io/support + description: This is a sample server for updating product information. + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + termsOfService: http://swagger.io/terms/ + title: Swagger Example API + version: "1.0" +paths: + /testapi/update-product/{product_id}: + post: + consumes: + - application/json + operationId: update-product + parameters: + - description: Product ID + in: path + name: product_id + required: true + type: integer + - description: ' ' + in: body + name: _ + required: true + schema: + $ref: '#/definitions/main.ProductUpdates' + responses: {} + summary: Update product attributes +swagger: "2.0" diff --git a/example/override/handler.go b/example/override/handler.go new file mode 100644 index 000000000..ef14e7f06 --- /dev/null +++ b/example/override/handler.go @@ -0,0 +1,31 @@ +package main + +import ( + "database/sql" + "encoding/json" + "net/http" +) + +type ProductUpdates struct { + Type sql.NullString `json:"type"` + Description sql.NullString `json:"description"` + Stock sql.NullInt64 `json:"stock"` +} + +// UpdateProduct example +// +// @Summary Update product attributes +// @ID update-product +// @Accept json +// @Param product_id path int true "Product ID" +// @Param _ body ProductUpdates true " " +// @Router /testapi/update-product/{product_id} [post] +func UpdateProduct(w http.ResponseWriter, r *http.Request) { + var pUpdates ProductUpdates + if err := json.NewDecoder(r.Body).Decode(&pUpdates); err != nil { + // write your code + return + } + + // write your code +} diff --git a/example/override/main.go b/example/override/main.go new file mode 100644 index 000000000..bd80adbf7 --- /dev/null +++ b/example/override/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "net/http" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server with null types overridden with primitive types. +// @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 product_info.swagger.io +// @BasePath /v2 + +func main() { + http.HandleFunc("/testapi/update-product", UpdateProduct) + http.ListenAndServe(":8080", nil) +} diff --git a/field_parser.go b/field_parser.go index bd4fa4218..beee28ac3 100644 --- a/field_parser.go +++ b/field_parser.go @@ -10,7 +10,6 @@ import ( "sync" "unicode" - "github.com/go-openapi/jsonreference" "github.com/go-openapi/spec" ) @@ -18,6 +17,7 @@ var _ FieldParser = &tagBaseFieldParser{p: nil, field: nil, tag: ""} const ( requiredLabel = "required" + optionalLabel = "optional" swaggerTypeTag = "swaggertype" swaggerIgnoreTag = "swaggerignore" ) @@ -43,7 +43,7 @@ func newTagBaseFieldParser(p *Parser, field *ast.Field) FieldParser { func (ps *tagBaseFieldParser) ShouldSkip() bool { // Skip non-exported fields. - if !ast.IsExported(ps.field.Names[0].Name) { + if ps.field.Names != nil && !ast.IsExported(ps.field.Names[0].Name) { return true } @@ -76,6 +76,10 @@ func (ps *tagBaseFieldParser) FieldName() (string, error) { } } + if ps.field.Names == nil { + return "", nil + } + switch ps.p.PropNamingStrategy { case SnakeCase: return toSnakeCase(ps.field.Names[0].Name), nil @@ -206,12 +210,30 @@ func splitNotWrapped(s string, sep rune) []string { return result } +// ComplementSchema complement schema with field properties func (ps *tagBaseFieldParser) ComplementSchema(schema *spec.Schema) error { types := ps.p.GetSchemaTypePath(schema, 2) if len(types) == 0 { return fmt.Errorf("invalid type for field: %s", ps.field.Names[0]) } + if IsRefSchema(schema) { + var newSchema = spec.Schema{} + err := ps.complementSchema(&newSchema, types) + if err != nil { + return err + } + if !reflect.ValueOf(newSchema).IsZero() { + *schema = *(newSchema.WithAllOf(*schema)) + } + return nil + } + + return ps.complementSchema(schema, types) +} + +// complementSchema complement schema with field properties +func (ps *tagBaseFieldParser) complementSchema(schema *spec.Schema, types []string) error { if ps.field.Tag == nil { if ps.field.Doc != nil { schema.Description = strings.TrimSpace(ps.field.Doc.Text()) @@ -352,19 +374,6 @@ func (ps *tagBaseFieldParser) ComplementSchema(schema *spec.Schema) error { schema.ReadOnly = ps.tag.Get(readOnlyTag) == "true" - if !reflect.ValueOf(schema.Ref).IsZero() && schema.ReadOnly { - schema.AllOf = []spec.Schema{*spec.RefSchema(schema.Ref.String())} - schema.Ref = spec.Ref{ - Ref: jsonreference.Ref{ - HasFullURL: false, - HasURLPathOnly: false, - HasFragmentOnly: false, - HasFileScheme: false, - HasFullFilePath: false, - }, - } // clear out existing ref - } - defaultTagValue := ps.tag.Get(defaultTag) if defaultTagValue != "" { value, err := defineType(field.schemaType, defaultTagValue) @@ -404,13 +413,13 @@ func (ps *tagBaseFieldParser) ComplementSchema(schema *spec.Schema) error { if schema.Items.Schema.Extensions == nil { schema.Items.Schema.Extensions = map[string]interface{}{} } - schema.Items.Schema.Extensions["x-enum-varnames"] = field.enumVarNames + schema.Items.Schema.Extensions[enumVarNamesExtension] = field.enumVarNames } else { // Add to top level schema if schema.Extensions == nil { schema.Extensions = map[string]interface{}{} } - schema.Extensions["x-enum-varnames"] = field.enumVarNames + schema.Extensions[enumVarNamesExtension] = field.enumVarNames } } @@ -472,8 +481,11 @@ func (ps *tagBaseFieldParser) IsRequired() (bool, error) { bindingTag := ps.tag.Get(bindingTag) if bindingTag != "" { for _, val := range strings.Split(bindingTag, ",") { - if val == requiredLabel { + switch val { + case requiredLabel: return true, nil + case optionalLabel: + return false, nil } } } @@ -481,13 +493,16 @@ func (ps *tagBaseFieldParser) IsRequired() (bool, error) { validateTag := ps.tag.Get(validateTag) if validateTag != "" { for _, val := range strings.Split(validateTag, ",") { - if val == requiredLabel { + switch val { + case requiredLabel: return true, nil + case optionalLabel: + return false, nil } } } - return false, nil + return ps.p.RequiredByDefault, nil } func parseValidTags(validTag string, sf *structField) { diff --git a/field_parser_test.go b/field_parser_test.go index aa89d6ac8..a7487c03d 100644 --- a/field_parser_test.go +++ b/field_parser_test.go @@ -82,6 +82,47 @@ func TestDefaultFieldParser(t *testing.T) { assert.Equal(t, true, got) }) + t.Run("Default required tag", func(t *testing.T) { + t.Parallel() + + got, err := newTagBaseFieldParser( + &Parser{ + RequiredByDefault: true, + }, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test"`, + }}, + ).IsRequired() + assert.NoError(t, err) + assert.True(t, got) + }) + + t.Run("Optional tag", func(t *testing.T) { + t.Parallel() + + got, err := newTagBaseFieldParser( + &Parser{ + RequiredByDefault: true, + }, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" binding:"optional"`, + }}, + ).IsRequired() + assert.NoError(t, err) + assert.False(t, got) + + got, err = newTagBaseFieldParser( + &Parser{ + RequiredByDefault: true, + }, + &ast.Field{Tag: &ast.BasicLit{ + Value: `json:"test" validate:"optional"`, + }}, + ).IsRequired() + assert.NoError(t, err) + assert.False(t, got) + }) + t.Run("Extensions tag", func(t *testing.T) { t.Parallel() diff --git a/format/format.go b/format/format.go index 9421e0605..e276be0e6 100644 --- a/format/format.go +++ b/format/format.go @@ -2,7 +2,6 @@ package format import ( "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -91,7 +90,7 @@ func (f *Format) excludeFile(path string) bool { } func (f *Format) format(path string) error { - contents, err := ioutil.ReadFile(path) + contents, err := os.ReadFile(path) if err != nil { return err } @@ -103,7 +102,7 @@ func (f *Format) format(path string) error { } func write(path string, contents []byte) error { - f, err := ioutil.TempFile(filepath.Split(path)) + f, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)) if err != nil { return err } diff --git a/format/format_test.go b/format/format_test.go index 508111e2b..152f6124a 100644 --- a/format/format_test.go +++ b/format/format_test.go @@ -2,7 +2,6 @@ package format import ( "bytes" - "io/ioutil" "os" "path/filepath" "testing" @@ -44,7 +43,7 @@ func TestFormat_DefaultExcludes(t *testing.T) { func TestFormat_ParseError(t *testing.T) { fx := setup(t) - ioutil.WriteFile(filepath.Join(fx.basedir, "parse_error.go"), []byte(`package main + os.WriteFile(filepath.Join(fx.basedir, "parse_error.go"), []byte(`package main func invalid() {`), 0644) assert.Error(t, New().Build(&Config{SearchDir: fx.basedir})) } @@ -82,7 +81,7 @@ func setup(t *testing.T) *fixture { if err := os.MkdirAll(filepath.Dir(fullpath), 0755); err != nil { t.Fatal(err) } - if err := ioutil.WriteFile(fullpath, contents, 0644); err != nil { + if err := os.WriteFile(fullpath, contents, 0644); err != nil { t.Fatal(err) } } @@ -90,7 +89,7 @@ func setup(t *testing.T) *fixture { } func (fx *fixture) isFormatted(file string) bool { - contents, err := ioutil.ReadFile(filepath.Join(fx.basedir, filepath.Clean(file))) + contents, err := os.ReadFile(filepath.Join(fx.basedir, filepath.Clean(file))) if err != nil { fx.t.Fatal(err) } diff --git a/formatter.go b/formatter.go index ca3e24c0f..511e3a822 100644 --- a/formatter.go +++ b/formatter.go @@ -2,21 +2,18 @@ package swag import ( "bytes" - "crypto/md5" "fmt" "go/ast" goparser "go/parser" "go/token" - "io" "log" "os" "regexp" + "sort" "strings" "text/tabwriter" ) -const splitTag = "&*" - // Check of @Param @Success @Failure @Response @Header var specialTagForSplit = map[string]bool{ paramAttr: true, @@ -55,60 +52,113 @@ func (f *Formatter) Format(fileName string, contents []byte) ([]byte, error) { if err != nil { return nil, err } - formattedComments := bytes.Buffer{} - oldComments := map[string]string{} - if ast.Comments != nil { - for _, comment := range ast.Comments { - formatFuncDoc(comment.List, &formattedComments, oldComments) - } + // Formatting changes are described as an edit list of byte range + // replacements. We make these content-level edits directly rather than + // changing the AST nodes and writing those out (via [go/printer] or + // [go/format]) so that we only change the formatting of Swag attribute + // comments. This won't touch the formatting of any other comments, or of + // functions, etc. + maxEdits := 0 + for _, comment := range ast.Comments { + maxEdits += len(comment.List) } - return formatComments(fileName, contents, formattedComments.Bytes(), oldComments), nil + edits := make(edits, 0, maxEdits) + + for _, comment := range ast.Comments { + formatFuncDoc(fileSet, comment.List, &edits) + } + + return edits.apply(contents), nil } -func formatComments(fileName string, contents []byte, formattedComments []byte, oldComments map[string]string) []byte { - for _, comment := range bytes.Split(formattedComments, []byte("\n")) { - splits := bytes.SplitN(comment, []byte(splitTag), 2) - if len(splits) == 2 { - hash, line := splits[0], splits[1] - contents = bytes.Replace(contents, []byte(oldComments[string(hash)]), line, 1) - } +type edit struct { + begin int + end int + replacement []byte +} + +type edits []edit + +func (edits edits) apply(contents []byte) []byte { + // Apply the edits with the highest offset first, so that earlier edits + // don't affect the offsets of later edits. + sort.Slice(edits, func(i, j int) bool { + return edits[i].begin > edits[j].begin + }) + + for _, edit := range edits { + prefix := contents[:edit.begin] + suffix := contents[edit.end:] + contents = append(prefix, append(edit.replacement, suffix...)...) } + return contents } -func formatFuncDoc(commentList []*ast.Comment, formattedComments io.Writer, oldCommentsMap map[string]string) { - w := tabwriter.NewWriter(formattedComments, 0, 0, 2, ' ', 0) +// formatFuncDoc reformats the comment lines in commentList, and appends any +// changes to the edit list. +func formatFuncDoc(fileSet *token.FileSet, commentList []*ast.Comment, edits *edits) { + // Building the edit list to format a comment block is a two-step process. + // First, we iterate over each comment line looking for Swag attributes. In + // each one we find, we replace alignment whitespace with a tab character, + // then write the result into a tab writer. + + linesToComments := make(map[int]int, len(commentList)) + + buffer := &bytes.Buffer{} + w := tabwriter.NewWriter(buffer, 1, 4, 1, '\t', 0) - for _, comment := range commentList { + for commentIndex, comment := range commentList { text := comment.Text if attr, body, found := swagComment(text); found { - cmd5 := fmt.Sprintf("%x", md5.Sum([]byte(text))) - oldCommentsMap[cmd5] = text - - formatted := "// " + attr + formatted := "//\t" + attr if body != "" { formatted += "\t" + splitComment2(attr, body) } - // md5 + splitTag + srcCommentLine - // eg. xxx&*@Description get struct array - _, _ = fmt.Fprintln(w, cmd5+splitTag+formatted) + _, _ = fmt.Fprintln(w, formatted) + linesToComments[len(linesToComments)] = commentIndex } } - // format by tabwriter + + // Once we've loaded all of the comment lines to be aligned into the tab + // writer, flushing it causes the aligned text to be written out to the + // backing buffer. _ = w.Flush() + + // Now the second step: we iterate over the aligned comment lines that were + // written into the backing buffer, pair each one up to its original + // comment line, and use the combination to describe the edit that needs to + // be made to the original input. + formattedComments := bytes.Split(buffer.Bytes(), []byte("\n")) + for lineIndex, commentIndex := range linesToComments { + comment := commentList[commentIndex] + *edits = append(*edits, edit{ + begin: fileSet.Position(comment.Pos()).Offset, + end: fileSet.Position(comment.End()).Offset, + replacement: formattedComments[lineIndex], + }) + } } func splitComment2(attr, body string) string { if specialTagForSplit[strings.ToLower(attr)] { for i := 0; i < len(body); i++ { if skipEnd, ok := skipChar[body[i]]; ok { - if skipLen := strings.IndexByte(body[i+1:], skipEnd); skipLen > 0 { - i += skipLen + skipStart, n := body[i], 1 + for i++; i < len(body); i++ { + if skipStart != skipEnd && body[i] == skipStart { + n++ + } else if body[i] == skipEnd { + n-- + if n == 0 { + break + } + } } - } else if body[i] == ' ' { + } else if body[i] == ' ' || body[i] == '\t' { j := i - for ; j < len(body) && body[j] == ' '; j++ { + for ; j < len(body) && (body[j] == ' ' || body[j] == '\t'); j++ { } body = replaceRange(body, i, j, "\t") } diff --git a/formatter_test.go b/formatter_test.go index 8cc585663..4c650a986 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -64,48 +64,77 @@ func Test_FormatMain(t *testing.T) { func main() {}` want := `package main - // @title Swagger Example API - // @version 1.0 - // @description This is a sample server Petstore server. - // @termsOfService http://swagger.io/terms/ - - // @contact.name API Support - // @contact.url http://www.swagger.io/support - // @contact.email support@swagger.io - - // @license.name Apache 2.0 - // @license.url http://www.apache.org/licenses/LICENSE-2.0.html - - // @host petstore.swagger.io - // @BasePath /v2 - - // @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 + // @title Swagger Example API + // @version 1.0 + // @description This is a sample server Petstore server. + // @termsOfService http://swagger.io/terms/ + + // @contact.name API Support + // @contact.url http://www.swagger.io/support + // @contact.email support@swagger.io + + // @license.name Apache 2.0 + // @license.url http://www.apache.org/licenses/LICENSE-2.0.html + + // @host petstore.swagger.io + // @BasePath /v2 + + // @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 func main() {}` + testFormat(t, "main.go", contents, want) +} + +func Test_FormatMultipleFunctions(t *testing.T) { + contents := `package main + + // @Produce json + // @Success 200 {object} string + // @Failure 400 {object} string + func A() {} + + // @Description Description of B. + // @Produce json + // @Success 200 {array} string + // @Failure 400 {object} string + func B() {}` + + want := `package main + + // @Produce json + // @Success 200 {object} string + // @Failure 400 {object} string + func A() {} + + // @Description Description of B. + // @Produce json + // @Success 200 {array} string + // @Failure 400 {object} string + func B() {}` testFormat(t, "main.go", contents, want) } @@ -132,17 +161,17 @@ func Test_FormatApi(t *testing.T) { 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 web.Pet true "Some ID" - // @Success 200 {string} string "ok" - // @Failure 400 {object} web.APIError "We need ID!!" - // @Failure 404 {object} web.APIError "Can not find ID" - // @Router /testapi/get-string-by-int/{some_id} [get] + // @Summary Add a new pet to the store + // @Description get string by ID + // @ID get-string-by-int + // @Accept json + // @Produce json + // @Param some_id path int true "Some ID" Format(int64) + // @Param some_id body web.Pet true "Some ID" + // @Success 200 {string} string "ok" + // @Failure 400 {object} web.APIError "We need ID!!" + // @Failure 404 {object} web.APIError "Can not find ID" + // @Router /testapi/get-string-by-int/{some_id} [get] func GetStringByInt(w http.ResponseWriter, r *http.Request) {}` testFormat(t, "api.go", contents, want) @@ -156,9 +185,9 @@ func Test_NonSwagComment(t *testing.T) { // @ Accept json // This is not a @swag comment` want := `package api - // @Summary Add a new pet to the store - // @Description get string by ID - // @ID get-string-by-int + // @Summary Add a new pet to the store + // @Description get string by ID + // @ID get-string-by-int // @ Accept json // This is not a @swag comment` @@ -170,8 +199,8 @@ func Test_EmptyComment(t *testing.T) { // @Summary Add a new pet to the store // @Description ` want := `package empty - // @Summary Add a new pet to the store - // @Description` + // @Summary Add a new pet to the store + // @Description` testFormat(t, "empty.go", contents, want) } @@ -181,8 +210,8 @@ func Test_AlignAttribute(t *testing.T) { // @Summary Add a new pet to the store // @Description Description` want := `package align - // @Summary Add a new pet to the store - // @Description Description` + // @Summary Add a new pet to the store + // @Description Description` testFormat(t, "align.go", contents, want) @@ -195,3 +224,45 @@ func Test_SyntaxError(t *testing.T) { _, err := NewFormatter().Format("invalid.go", contents) assert.Error(t, err) } + +func Test_splitComment2(t *testing.T) { + type args struct { + attr string + body string + } + tests := []struct { + name string + args args + want string + }{ + { + "test_splitComment2_1", + args{ + attr: "@param", + body: " data body web.GenericBodyMulti[[]types.Post, [][]types.Post]", + }, + "\tdata\tbody\tweb.GenericBodyMulti[[]types.Post, [][]types.Post]", + }, + { + "test_splitComment2_2", + args{ + attr: "@param", + body: ` some_id path int true "Some ID" Format(int64)`, + }, + "\tsome_id\tpath\tint\ttrue\t\"Some ID\"\tFormat(int64)", + }, + { + "test_splitComment2_3", + args{ + attr: "@param", + body: ` @Param some_id body web.Pet true "Some ID"`, + }, + "\t@Param\tsome_id\tbody\tweb.Pet\ttrue\t\"Some ID\"", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, splitComment2(tt.args.attr, tt.args.body), "splitComment2(%v, %v)", tt.args.attr, tt.args.body) + }) + } +} diff --git a/gen/gen.go b/gen/gen.go index 3afbc2b6c..d74202623 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -33,6 +33,12 @@ type Gen struct { jsonIndent func(data interface{}) ([]byte, error) jsonToYAML func(data []byte) ([]byte, error) outputTypeMap map[string]genTypeWriter + debug Debugger +} + +// Debugger is the interface that wraps the basic Printf method. +type Debugger interface { + Printf(format string, v ...interface{}) } // New creates a new Gen. @@ -43,6 +49,7 @@ func New() *Gen { return json.MarshalIndent(data, "", " ") }, jsonToYAML: yaml.JSONToYAML, + debug: log.New(os.Stdout, "", log.LstdFlags), } gen.outputTypeMap = map[string]genTypeWriter{ @@ -108,15 +115,24 @@ type Config struct { // GeneratedTime whether swag should generate the timestamp at the top of docs.go GeneratedTime bool + // RequiredByDefault set validation required for all fields by default + RequiredByDefault bool + // OverridesFile defines global type overrides. OverridesFile string // ParseGoList whether swag use go list to parse dependency ParseGoList bool + + // include only tags mentioned when searching, comma separated + Tags string } // Build builds swagger json file for given searchDir and mainAPIFile. Returns json. func (g *Gen) Build(config *Config) error { + if config.Debugger != nil { + g.debug = config.Debugger + } if config.InstanceName == "" { config.InstanceName = swag.Name } @@ -138,7 +154,7 @@ func (g *Gen) Build(config *Config) error { return fmt.Errorf("could not open overrides file: %w", err) } } else { - log.Printf("Using overrides from %s", config.OverridesFile) + g.debug.Printf("Using overrides from %s", config.OverridesFile) overrides, err = parseOverrides(overridesFile) if err != nil { @@ -147,9 +163,11 @@ func (g *Gen) Build(config *Config) error { } } - log.Println("Generate swagger docs....") + g.debug.Printf("Generate swagger docs....") - p := swag.New(swag.SetMarkdownFileDirectory(config.MarkdownFilesDir), + p := swag.New( + swag.SetParseDependency(config.ParseDependency), + swag.SetMarkdownFileDirectory(config.MarkdownFilesDir), swag.SetDebugger(config.Debugger), swag.SetExcludedDirsAndFiles(config.Excludes), swag.SetParseExtension(config.ParseExtension), @@ -157,12 +175,13 @@ func (g *Gen) Build(config *Config) error { swag.SetStrict(config.Strict), swag.SetOverrides(overrides), swag.ParseUsingGoList(config.ParseGoList), + swag.SetTags(config.Tags), ) p.PropNamingStrategy = config.PropNamingStrategy p.ParseVendor = config.ParseVendor - p.ParseDependency = config.ParseDependency p.ParseInternal = config.ParseInternal + p.RequiredByDefault = config.RequiredByDefault if err := p.ParseAPIMultiSearchDir(searchDirs, config.MainAPIFile, config.ParseDepth); err != nil { return err @@ -216,7 +235,7 @@ func (g *Gen) writeDocSwagger(config *Config, swagger *spec.Swagger) error { return err } - log.Printf("create docs.go at %+v", docFileName) + g.debug.Printf("create docs.go at %+v", docFileName) return nil } @@ -240,7 +259,7 @@ func (g *Gen) writeJSONSwagger(config *Config, swagger *spec.Swagger) error { return err } - log.Printf("create swagger.json at %+v", jsonFileName) + g.debug.Printf("create swagger.json at %+v", jsonFileName) return nil } @@ -269,7 +288,7 @@ func (g *Gen) writeYAMLSwagger(config *Config, swagger *spec.Swagger) error { return err } - log.Printf("create swagger.yaml at %+v", yamlFileName) + g.debug.Printf("create swagger.yaml at %+v", yamlFileName) return nil } diff --git a/gen/gen_test.go b/gen/gen_test.go index 62a26c2d3..35acd6bf3 100644 --- a/gen/gen_test.go +++ b/gen/gen_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "log" "os" "os/exec" @@ -95,7 +94,7 @@ func TestGen_BuildInstanceName(t *testing.T) { goSourceFile := filepath.Join(config.OutputDir, "docs.go") // Validate default registration name - expectedCode, err := ioutil.ReadFile(goSourceFile) + expectedCode, err := os.ReadFile(goSourceFile) if err != nil { require.NoError(t, err) } @@ -119,7 +118,7 @@ func TestGen_BuildInstanceName(t *testing.T) { goSourceFile = filepath.Join(config.OutputDir, config.InstanceName+"_"+"docs.go") assert.NoError(t, New().Build(config)) - expectedCode, err = ioutil.ReadFile(goSourceFile) + expectedCode, err = os.ReadFile(goSourceFile) if err != nil { require.NoError(t, err) } @@ -254,7 +253,7 @@ func TestGen_BuildDescriptionWithQuotes(t *testing.T) { require.NoError(t, err) } - expectedJSON, err := ioutil.ReadFile(filepath.Join(config.SearchDir, "expected.json")) + expectedJSON, err := os.ReadFile(filepath.Join(config.SearchDir, "expected.json")) if err != nil { require.NoError(t, err) } @@ -641,6 +640,20 @@ func TestGen_parseOverrides(t *testing.T) { "github.com/foo/bar": "", }, }, + { + Name: "generic-simple", + Data: `replace types.Field[string] string`, + Expected: map[string]string{ + "types.Field[string]": "string", + }, + }, + { + Name: "generic-double", + Data: `replace types.Field[string,string] string`, + Expected: map[string]string{ + "types.Field[string,string]": "string", + }, + }, { Name: "comment", Data: `// this is a comment @@ -680,7 +693,7 @@ func TestGen_parseOverrides(t *testing.T) { func TestGen_TypeOverridesFile(t *testing.T) { customPath := "/foo/bar/baz" - tmp, err := ioutil.TempFile("", "") + tmp, err := os.CreateTemp("", "") require.NoError(t, err) defer os.Remove(tmp.Name()) @@ -779,3 +792,45 @@ func TestGen_Debugger(t *testing.T) { _ = os.Remove(expectedFile) } } + +func TestGen_ErrorAndInterface(t *testing.T) { + config := &Config{ + SearchDir: "../testdata/error", + MainAPIFile: "./main.go", + OutputDir: "../testdata/error/docs", + OutputTypes: outputTypes, + PropNamingStrategy: "", + } + + 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"), + } + t.Cleanup(func() { + for _, expectedFile := range expectedFiles { + _ = os.Remove(expectedFile) + } + }) + + // check files + for _, expectedFile := range expectedFiles { + if _, err := os.Stat(expectedFile); os.IsNotExist(err) { + require.NoError(t, err) + } + } + + // check content + jsonOutput, err := os.ReadFile(filepath.Join(config.OutputDir, "swagger.json")) + if err != nil { + require.NoError(t, err) + } + expectedJSON, err := os.ReadFile(filepath.Join(config.SearchDir, "expected.json")) + if err != nil { + require.NoError(t, err) + } + + assert.JSONEq(t, string(expectedJSON), string(jsonOutput)) +} diff --git a/generics.go b/generics.go new file mode 100644 index 000000000..652f4a8d0 --- /dev/null +++ b/generics.go @@ -0,0 +1,347 @@ +//go:build go1.18 +// +build go1.18 + +package swag + +import ( + "errors" + "fmt" + "go/ast" + "strings" + "unicode" + + "github.com/go-openapi/spec" +) + +type genericTypeSpec struct { + ArrayDepth int + TypeSpec *TypeSpecDef + Name string +} + +func (t *genericTypeSpec) TypeName() string { + if t.TypeSpec != nil { + return t.TypeSpec.TypeName() + } + return t.Name +} + +func (pkgDefs *PackagesDefinitions) parametrizeGenericType(file *ast.File, original *TypeSpecDef, fullGenericForm string) *TypeSpecDef { + if original == nil || original.TypeSpec.TypeParams == nil || len(original.TypeSpec.TypeParams.List) == 0 { + return original + } + + name, genericParams := splitGenericsTypeName(fullGenericForm) + if genericParams == nil { + return nil + } + + genericParamTypeDefs := map[string]*genericTypeSpec{} + if len(genericParams) != len(original.TypeSpec.TypeParams.List) { + return nil + } + + for i, genericParam := range genericParams { + arrayDepth := 0 + for { + if len(genericParam) <= 2 || genericParam[:2] != "[]" { + break + } + genericParam = genericParam[2:] + arrayDepth++ + } + + typeDef := pkgDefs.FindTypeSpec(genericParam, file) + if typeDef != nil { + genericParam = typeDef.TypeName() + if _, ok := pkgDefs.uniqueDefinitions[genericParam]; !ok { + pkgDefs.uniqueDefinitions[genericParam] = typeDef + } + } + + genericParamTypeDefs[original.TypeSpec.TypeParams.List[i].Names[0].Name] = &genericTypeSpec{ + ArrayDepth: arrayDepth, + TypeSpec: typeDef, + Name: genericParam, + } + } + + name = fmt.Sprintf("%s%s-", string(IgnoreNameOverridePrefix), original.TypeName()) + var nameParts []string + for _, def := range original.TypeSpec.TypeParams.List { + if specDef, ok := genericParamTypeDefs[def.Names[0].Name]; ok { + var prefix = "" + if specDef.ArrayDepth == 1 { + prefix = "array_" + } else if specDef.ArrayDepth > 1 { + prefix = fmt.Sprintf("array%d_", specDef.ArrayDepth) + } + nameParts = append(nameParts, prefix+specDef.TypeName()) + } + } + + name += strings.Replace(strings.Join(nameParts, "-"), ".", "_", -1) + + if typeSpec, ok := pkgDefs.uniqueDefinitions[name]; ok { + return typeSpec + } + + parametrizedTypeSpec := &TypeSpecDef{ + File: original.File, + PkgPath: original.PkgPath, + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{ + Name: name, + NamePos: original.TypeSpec.Name.NamePos, + Obj: original.TypeSpec.Name.Obj, + }, + Type: pkgDefs.resolveGenericType(original.File, original.TypeSpec.Type, genericParamTypeDefs), + Doc: original.TypeSpec.Doc, + Assign: original.TypeSpec.Assign, + }, + } + pkgDefs.uniqueDefinitions[name] = parametrizedTypeSpec + + return parametrizedTypeSpec +} + +// splitGenericsTypeName splits a generic struct name in his parts +func splitGenericsTypeName(fullGenericForm string) (string, []string) { + //remove all spaces character + fullGenericForm = strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 + } + return r + }, fullGenericForm) + + // split only at the first '[' and remove the last ']' + if fullGenericForm[len(fullGenericForm)-1] != ']' { + return "", nil + } + + genericParams := strings.SplitN(fullGenericForm[:len(fullGenericForm)-1], "[", 2) + if len(genericParams) == 1 { + return "", nil + } + + // generic type name + genericTypeName := genericParams[0] + + depth := 0 + genericParams = strings.FieldsFunc(genericParams[1], func(r rune) bool { + if r == '[' { + depth++ + } else if r == ']' { + depth-- + } else if r == ',' && depth == 0 { + return true + } + return false + }) + if depth != 0 { + return "", nil + } + + return genericTypeName, genericParams +} + +func (pkgDefs *PackagesDefinitions) getParametrizedType(genTypeSpec *genericTypeSpec) ast.Expr { + if genTypeSpec.TypeSpec != nil && strings.Contains(genTypeSpec.Name, ".") { + parts := strings.SplitN(genTypeSpec.Name, ".", 2) + return &ast.SelectorExpr{ + X: &ast.Ident{Name: parts[0]}, + Sel: &ast.Ident{Name: parts[1]}, + } + } + + //a primitive type name or a type name in current package + return &ast.Ident{Name: genTypeSpec.Name} +} + +func (pkgDefs *PackagesDefinitions) resolveGenericType(file *ast.File, expr ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) ast.Expr { + switch astExpr := expr.(type) { + case *ast.Ident: + if genTypeSpec, ok := genericParamTypeDefs[astExpr.Name]; ok { + retType := pkgDefs.getParametrizedType(genTypeSpec) + for i := 0; i < genTypeSpec.ArrayDepth; i++ { + retType = &ast.ArrayType{Elt: retType} + } + return retType + } + case *ast.ArrayType: + return &ast.ArrayType{ + Elt: pkgDefs.resolveGenericType(file, astExpr.Elt, genericParamTypeDefs), + Len: astExpr.Len, + Lbrack: astExpr.Lbrack, + } + case *ast.StarExpr: + return &ast.StarExpr{ + Star: astExpr.Star, + X: pkgDefs.resolveGenericType(file, astExpr.X, genericParamTypeDefs), + } + case *ast.IndexExpr, *ast.IndexListExpr: + fullGenericName, _ := getGenericFieldType(file, expr, genericParamTypeDefs) + typeDef := pkgDefs.FindTypeSpec(fullGenericName, file) + if typeDef != nil { + return typeDef.TypeSpec.Type + } + case *ast.StructType: + newStructTypeDef := &ast.StructType{ + Struct: astExpr.Struct, + Incomplete: astExpr.Incomplete, + Fields: &ast.FieldList{ + Opening: astExpr.Fields.Opening, + Closing: astExpr.Fields.Closing, + }, + } + + for _, field := range astExpr.Fields.List { + newField := &ast.Field{ + Type: field.Type, + Doc: field.Doc, + Names: field.Names, + Tag: field.Tag, + Comment: field.Comment, + } + + newField.Type = pkgDefs.resolveGenericType(file, field.Type, genericParamTypeDefs) + + newStructTypeDef.Fields.List = append(newStructTypeDef.Fields.List, newField) + } + return newStructTypeDef + } + return expr +} + +func getExtendedGenericFieldType(file *ast.File, field ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) (string, error) { + switch fieldType := field.(type) { + case *ast.ArrayType: + fieldName, err := getExtendedGenericFieldType(file, fieldType.Elt, genericParamTypeDefs) + return "[]" + fieldName, err + case *ast.StarExpr: + return getExtendedGenericFieldType(file, fieldType.X, genericParamTypeDefs) + case *ast.Ident: + if genericParamTypeDefs != nil { + if typeSpec, ok := genericParamTypeDefs[fieldType.Name]; ok { + return typeSpec.Name, nil + } + } + if fieldType.Obj == nil { + return fieldType.Name, nil + } + + tSpec := &TypeSpecDef{ + File: file, + TypeSpec: fieldType.Obj.Decl.(*ast.TypeSpec), + PkgPath: file.Name.Name, + } + return tSpec.TypeName(), nil + default: + return getFieldType(file, field) + } +} + +func getGenericFieldType(file *ast.File, field ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) (string, error) { + var fullName string + var baseName string + var err error + switch fieldType := field.(type) { + case *ast.IndexListExpr: + baseName, err = getGenericTypeName(file, fieldType.X) + if err != nil { + return "", err + } + fullName = baseName + "[" + + for _, index := range fieldType.Indices { + fieldName, err := getExtendedGenericFieldType(file, index, genericParamTypeDefs) + if err != nil { + return "", err + } + + fullName += fieldName + "," + } + + fullName = strings.TrimRight(fullName, ",") + "]" + case *ast.IndexExpr: + baseName, err = getGenericTypeName(file, fieldType.X) + if err != nil { + return "", err + } + + indexName, err := getExtendedGenericFieldType(file, fieldType.Index, genericParamTypeDefs) + if err != nil { + return "", err + } + + fullName = fmt.Sprintf("%s[%s]", baseName, indexName) + } + + if fullName == "" { + return "", fmt.Errorf("unknown field type %#v", field) + } + + var packageName string + if !strings.Contains(baseName, ".") { + if file.Name == nil { + return "", errors.New("file name is nil") + } + packageName, _ = getFieldType(file, file.Name) + } + + return strings.TrimLeft(fmt.Sprintf("%s.%s", packageName, fullName), "."), nil +} + +func getGenericTypeName(file *ast.File, field ast.Expr) (string, error) { + switch fieldType := field.(type) { + case *ast.Ident: + if fieldType.Obj == nil { + return fieldType.Name, nil + } + + tSpec := &TypeSpecDef{ + File: file, + TypeSpec: fieldType.Obj.Decl.(*ast.TypeSpec), + PkgPath: file.Name.Name, + } + return tSpec.TypeName(), nil + case *ast.ArrayType: + tSpec := &TypeSpecDef{ + File: file, + TypeSpec: fieldType.Elt.(*ast.Ident).Obj.Decl.(*ast.TypeSpec), + PkgPath: file.Name.Name, + } + return tSpec.TypeName(), nil + case *ast.SelectorExpr: + return fmt.Sprintf("%s.%s", fieldType.X.(*ast.Ident).Name, fieldType.Sel.Name), nil + } + return "", fmt.Errorf("unknown type %#v", field) +} + +func (parser *Parser) parseGenericTypeExpr(file *ast.File, typeExpr ast.Expr) (*spec.Schema, error) { + switch expr := typeExpr.(type) { + // suppress debug messages for these types + case *ast.InterfaceType: + case *ast.StructType: + case *ast.Ident: + case *ast.StarExpr: + case *ast.SelectorExpr: + case *ast.ArrayType: + case *ast.MapType: + case *ast.FuncType: + case *ast.IndexExpr: + name, err := getExtendedGenericFieldType(file, expr, nil) + if err == nil { + if schema, err := parser.getTypeSchema(name, file, false); err == nil { + return schema, nil + } + } + + parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead. (%s)\n", typeExpr, err) + default: + parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) + } + + return PrimitiveSchema(OBJECT), nil +} diff --git a/generics_other.go b/generics_other.go new file mode 100644 index 000000000..5fd9e8231 --- /dev/null +++ b/generics_other.go @@ -0,0 +1,42 @@ +//go:build !go1.18 +// +build !go1.18 + +package swag + +import ( + "fmt" + "github.com/go-openapi/spec" + "go/ast" +) + +type genericTypeSpec struct { + ArrayDepth int + TypeSpec *TypeSpecDef + Name string +} + +func (pkgDefs *PackagesDefinitions) parametrizeGenericType(file *ast.File, original *TypeSpecDef, fullGenericForm string) *TypeSpecDef { + return original +} + +func getGenericFieldType(file *ast.File, field ast.Expr, genericParamTypeDefs map[string]*genericTypeSpec) (string, error) { + return "", fmt.Errorf("unknown field type %#v", field) +} + +func (parser *Parser) parseGenericTypeExpr(file *ast.File, typeExpr ast.Expr) (*spec.Schema, error) { + switch typeExpr.(type) { + // suppress debug messages for these types + case *ast.InterfaceType: + case *ast.StructType: + case *ast.Ident: + case *ast.StarExpr: + case *ast.SelectorExpr: + case *ast.ArrayType: + case *ast.MapType: + case *ast.FuncType: + default: + parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) + } + + return PrimitiveSchema(OBJECT), nil +} diff --git a/generics_other_test.go b/generics_other_test.go new file mode 100644 index 000000000..1a396a04b --- /dev/null +++ b/generics_other_test.go @@ -0,0 +1,67 @@ +//go:build !go1.18 +// +build !go1.18 + +package swag + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "go/ast" + "testing" +) + +type testLogger struct { + Messages []string +} + +func (t *testLogger) Printf(format string, v ...interface{}) { + t.Messages = append(t.Messages, fmt.Sprintf(format, v...)) +} + +func TestParametrizeStruct(t *testing.T) { + t.Parallel() + + pd := PackagesDefinitions{ + packages: make(map[string]*PackageDefinitions), + } + + tSpec := &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }, + } + + tr := pd.parametrizeGenericType(&ast.File{}, tSpec, "") + assert.Equal(t, tr, tSpec) + + tr = pd.parametrizeGenericType(&ast.File{}, tSpec, "") + assert.Equal(t, tr, tSpec) +} + +func TestParseGenericTypeExpr(t *testing.T) { + t.Parallel() + + parser := New() + logger := &testLogger{} + SetDebugger(logger)(parser) + + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.InterfaceType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.StructType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.Ident{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.StarExpr{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.SelectorExpr{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.ArrayType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.MapType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.FuncType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.BadExpr{}) + assert.NotEmpty(t, logger.Messages) +} diff --git a/generics_test.go b/generics_test.go new file mode 100644 index 000000000..48dde59a9 --- /dev/null +++ b/generics_test.go @@ -0,0 +1,407 @@ +//go:build go1.18 +// +build go1.18 + +package swag + +import ( + "encoding/json" + "fmt" + "go/ast" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testLogger struct { + Messages []string +} + +func (t *testLogger) Printf(format string, v ...interface{}) { + t.Messages = append(t.Messages, fmt.Sprintf(format, v...)) +} + +func TestParseGenericsBasic(t *testing.T) { + t.Parallel() + + searchDir := "testdata/generics_basic" + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) + assert.NoError(t, err) + + p := New() + p.Overrides = map[string]string{ + "types.Field[string]": "string", + "types.DoubleField[string,string]": "[]string", + "types.TrippleField[string,string]": "[][]string", + } + + err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + b, err := json.MarshalIndent(p.swagger, "", " ") + assert.NoError(t, err) + assert.Equal(t, string(expected), string(b)) +} + +func TestParseGenericsArrays(t *testing.T) { + t.Parallel() + + searchDir := "testdata/generics_arrays" + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) + assert.NoError(t, err) + + p := New() + err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + b, err := json.MarshalIndent(p.swagger, "", " ") + assert.NoError(t, err) + assert.Equal(t, string(expected), string(b)) +} + +func TestParseGenericsNested(t *testing.T) { + t.Parallel() + + searchDir := "testdata/generics_nested" + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) + assert.NoError(t, err) + + p := New() + err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + b, err := json.MarshalIndent(p.swagger, "", " ") + assert.NoError(t, err) + assert.Equal(t, string(expected), string(b)) +} + +func TestParseGenericsProperty(t *testing.T) { + t.Parallel() + + searchDir := "testdata/generics_property" + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) + assert.NoError(t, err) + + p := New() + err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + b, err := json.MarshalIndent(p.swagger, "", " ") + assert.NoError(t, err) + assert.Equal(t, string(expected), string(b)) +} + +func TestParseGenericsNames(t *testing.T) { + t.Parallel() + + searchDir := "testdata/generics_names" + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) + assert.NoError(t, err) + + p := New() + err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + b, err := json.MarshalIndent(p.swagger, "", " ") + assert.NoError(t, err) + assert.Equal(t, string(expected), string(b)) +} + +func TestParseGenericsPackageAlias(t *testing.T) { + t.Parallel() + + searchDir := "testdata/generics_package_alias/internal" + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) + assert.NoError(t, err) + + p := New(SetParseDependency(true)) + err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + b, err := json.MarshalIndent(p.swagger, "", " ") + assert.NoError(t, err) + assert.Equal(t, string(expected), string(b)) +} + +func TestParametrizeStruct(t *testing.T) { + pd := PackagesDefinitions{ + packages: make(map[string]*PackageDefinitions), + uniqueDefinitions: make(map[string]*TypeSpecDef), + } + // valid + typeSpec := pd.parametrizeGenericType( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + File: &ast.File{Name: &ast.Ident{Name: "test"}}, + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string, []string]") + assert.NotNil(t, typeSpec) + assert.Equal(t, "$test.Field-string-array_string", typeSpec.Name()) + assert.Equal(t, "test.Field-string-array_string", typeSpec.TypeName()) + + // definition contains one type params, but two type params are provided + typeSpec = pd.parametrizeGenericType( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string, string]") + assert.Nil(t, typeSpec) + + // definition contains two type params, but only one is used + typeSpec = pd.parametrizeGenericType( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string]") + assert.Nil(t, typeSpec) + + // name is not a valid type name + typeSpec = pd.parametrizeGenericType( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string") + assert.Nil(t, typeSpec) + + typeSpec = pd.parametrizeGenericType( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string, [string]") + assert.Nil(t, typeSpec) + + typeSpec = pd.parametrizeGenericType( + &ast.File{Name: &ast.Ident{Name: "test2"}}, + &TypeSpecDef{ + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}, {Names: []*ast.Ident{{Name: "T2"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }}, "test.Field[string, ]string]") + assert.Nil(t, typeSpec) +} + +func TestSplitGenericsTypeNames(t *testing.T) { + t.Parallel() + + field, params := splitGenericsTypeName("test.Field") + assert.Empty(t, field) + assert.Nil(t, params) + + field, params = splitGenericsTypeName("test.Field]") + assert.Empty(t, field) + assert.Nil(t, params) + + field, params = splitGenericsTypeName("test.Field[string") + assert.Empty(t, field) + assert.Nil(t, params) + + field, params = splitGenericsTypeName("test.Field[string] ") + assert.Equal(t, "test.Field", field) + assert.Equal(t, []string{"string"}, params) + + field, params = splitGenericsTypeName("test.Field[string, []string]") + assert.Equal(t, "test.Field", field) + assert.Equal(t, []string{"string", "[]string"}, params) + + field, params = splitGenericsTypeName("test.Field[test.Field[ string, []string] ]") + assert.Equal(t, "test.Field", field) + assert.Equal(t, []string{"test.Field[string,[]string]"}, params) +} + +func TestGetGenericFieldType(t *testing.T) { + field, err := getFieldType( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexListExpr{ + X: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, + Indices: []ast.Expr{&ast.Ident{Name: "string"}}, + }, + ) + assert.NoError(t, err) + assert.Equal(t, "test.Field[string]", field) + + field, err = getFieldType( + &ast.File{Name: &ast.Ident{}}, + &ast.IndexListExpr{ + X: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, + Indices: []ast.Expr{&ast.Ident{Name: "string"}}, + }, + ) + assert.NoError(t, err) + assert.Equal(t, "Field[string]", field) + + field, err = getFieldType( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexListExpr{ + X: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, + Indices: []ast.Expr{&ast.Ident{Name: "string"}, &ast.Ident{Name: "int"}}, + }, + ) + assert.NoError(t, err) + assert.Equal(t, "test.Field[string,int]", field) + + field, err = getFieldType( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexListExpr{ + X: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, + Indices: []ast.Expr{&ast.Ident{Name: "string"}, &ast.ArrayType{Elt: &ast.Ident{Name: "int"}}}, + }, + ) + assert.NoError(t, err) + assert.Equal(t, "test.Field[string,[]int]", field) + + field, err = getFieldType( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexListExpr{ + X: &ast.BadExpr{}, + Indices: []ast.Expr{&ast.Ident{Name: "string"}, &ast.Ident{Name: "int"}}, + }, + ) + assert.Error(t, err) + + field, err = getFieldType( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexListExpr{ + X: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, + Indices: []ast.Expr{&ast.Ident{Name: "string"}, &ast.ArrayType{Elt: &ast.BadExpr{}}}, + }, + ) + assert.Error(t, err) + + field, err = getFieldType( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexExpr{X: &ast.Ident{Name: "Field"}, Index: &ast.Ident{Name: "string"}}, + ) + assert.NoError(t, err) + assert.Equal(t, "test.Field[string]", field) + + field, err = getFieldType( + &ast.File{Name: nil}, + &ast.IndexExpr{X: &ast.Ident{Name: "Field"}, Index: &ast.Ident{Name: "string"}}, + ) + assert.Error(t, err) + + field, err = getFieldType( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexExpr{X: &ast.BadExpr{}, Index: &ast.Ident{Name: "string"}}, + ) + assert.Error(t, err) + + field, err = getFieldType( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexExpr{X: &ast.Ident{Name: "Field"}, Index: &ast.BadExpr{}}, + ) + assert.Error(t, err) + + field, err = getFieldType( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexExpr{X: &ast.SelectorExpr{X: &ast.Ident{Name: "field"}, Sel: &ast.Ident{Name: "Name"}}, Index: &ast.Ident{Name: "string"}}, + ) + assert.NoError(t, err) + assert.Equal(t, "field.Name[string]", field) +} + +func TestGetGenericTypeName(t *testing.T) { + field, err := getGenericTypeName( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}, + ) + assert.NoError(t, err) + assert.Equal(t, "test.Field", field) + + field, err = getGenericTypeName( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.ArrayType{Elt: &ast.Ident{Name: "types", Obj: &ast.Object{Decl: &ast.TypeSpec{Name: &ast.Ident{Name: "Field"}}}}}, + ) + assert.NoError(t, err) + assert.Equal(t, "test.Field", field) + + field, err = getGenericTypeName( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.SelectorExpr{X: &ast.Ident{Name: "field"}, Sel: &ast.Ident{Name: "Name"}}, + ) + assert.NoError(t, err) + assert.Equal(t, "field.Name", field) + + _, err = getGenericTypeName( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.BadExpr{}, + ) + assert.Error(t, err) +} + +func TestParseGenericTypeExpr(t *testing.T) { + t.Parallel() + + parser := New() + logger := &testLogger{} + SetDebugger(logger)(parser) + + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.InterfaceType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.StructType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.Ident{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.StarExpr{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.SelectorExpr{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.ArrayType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.MapType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.FuncType{}) + assert.Empty(t, logger.Messages) + _, _ = parser.parseGenericTypeExpr(&ast.File{}, &ast.BadExpr{}) + assert.NotEmpty(t, logger.Messages) + assert.Len(t, logger.Messages, 1) + + parser.packages.uniqueDefinitions["field.Name[string]"] = &TypeSpecDef{ + File: &ast.File{Name: &ast.Ident{Name: "test"}}, + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{Name: "Field"}, + TypeParams: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{{Name: "T"}}}}}, + Type: &ast.StructType{Struct: 100, Fields: &ast.FieldList{Opening: 101, Closing: 102}}, + }, + } + spec, err := parser.parseTypeExpr( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexExpr{X: &ast.SelectorExpr{X: &ast.Ident{Name: "field"}, Sel: &ast.Ident{Name: "Name"}}, Index: &ast.Ident{Name: "string"}}, + false, + ) + assert.NotNil(t, spec) + assert.NoError(t, err) + + logger.Messages = []string{} + spec, err = parser.parseTypeExpr( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.IndexExpr{X: &ast.BadExpr{}, Index: &ast.Ident{Name: "string"}}, + false, + ) + assert.NotNil(t, spec) + assert.Equal(t, "object", spec.SchemaProps.Type[0]) + assert.NotEmpty(t, logger.Messages) + assert.Len(t, logger.Messages, 1) + + logger.Messages = []string{} + spec, err = parser.parseTypeExpr( + &ast.File{Name: &ast.Ident{Name: "test"}}, + &ast.BadExpr{}, + false, + ) + assert.NotNil(t, spec) + assert.Equal(t, "object", spec.SchemaProps.Type[0]) + assert.NotEmpty(t, logger.Messages) + assert.Len(t, logger.Messages, 1) +} diff --git a/go.mod b/go.mod index 99823bab5..dbc2eb3d4 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/go-openapi/spec v0.20.4 github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 - golang.org/x/tools v0.1.10 + golang.org/x/tools v0.1.12 ) require ( @@ -24,8 +24,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect - golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect diff --git a/go.sum b/go.sum index f2c35bf10..cfd283a76 100644 --- a/go.sum +++ b/go.sum @@ -49,22 +49,21 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= diff --git a/operation.go b/operation.go index e8675917d..bd817ba7a 100644 --- a/operation.go +++ b/operation.go @@ -6,7 +6,6 @@ import ( "go/ast" goparser "go/parser" "go/token" - "io/ioutil" "net/http" "os" "path/filepath" @@ -111,9 +110,13 @@ func (operation *Operation) ParseComment(comment string, astFile *ast.File) erro return nil } - attribute := strings.Fields(commentLine)[0] - lineRemainder, lowerAttribute := strings.TrimSpace(commentLine[len(attribute):]), strings.ToLower(attribute) - + fields := FieldsByAnySpace(commentLine, 2) + attribute := fields[0] + lowerAttribute := strings.ToLower(attribute) + var lineRemainder string + if len(fields) > 1 { + lineRemainder = fields[1] + } switch lowerAttribute { case descriptionAttr: operation.ParseDescriptionComment(lineRemainder) @@ -213,7 +216,7 @@ func (operation *Operation) ParseMetadata(attribute, lowerAttribute, lineRemaind return nil } -var paramPattern = regexp.MustCompile(`(\S+)\s+(\w+)\s+([\S.]+)\s+(\w+)\s+"([^"]+)"`) +var paramPattern = regexp.MustCompile(`(\S+)\s+(\w+)\s+([\S. ]+?)\s+(\w+)\s+"([^"]+)"`) func findInSlice(arr []string, target string) bool { for _, str := range arr { @@ -226,7 +229,7 @@ func findInSlice(arr []string, target string) bool { } func (operation *Operation) parseArrayParam(param *spec.Parameter, paramType, refType, objectType string) error { - if !IsPrimitiveType(refType) { + if !IsPrimitiveType(refType) && !(refType == "file" && paramType == "formData") { return fmt.Errorf("%s is not supported array type for %s", refType, paramType) } @@ -270,7 +273,9 @@ func (operation *Operation) parseArrayParam(param *spec.Parameter, paramType, re // ParseParamComment parses params return []string of param properties // E.g. @Param queryText formData string true "The email for login" -// [param name] [paramType] [data type] [is mandatory?] [Comment] +// +// [param name] [paramType] [data type] [is mandatory?] [Comment] +// // E.g. @Param some_id path int true "Some ID". func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.File) error { matches := paramPattern.FindStringSubmatch(commentLine) @@ -385,7 +390,7 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F if objectType == PRIMITIVE { param.Schema = PrimitiveSchema(refType) } else { - schema, err := operation.parseAPIObjectSchema(objectType, refType, astFile) + schema, err := operation.parseAPIObjectSchema(commentLine, objectType, refType, astFile) if err != nil { return err } @@ -818,12 +823,16 @@ func findTypeDef(importPath, typeName string) (*ast.TypeSpec, error) { return nil, fmt.Errorf("type spec not found") } -var responsePattern = regexp.MustCompile(`^([\w,]+)\s+([\w{}]+)\s+([\w\-.\\{}=,\[\]]+)[^"]*(.*)?`) +var responsePattern = regexp.MustCompile(`^([\w,]+)\s+([\w{}]+)\s+([\w\-.\\{}=,\[\s\]]+)\s*(".*)?`) // ResponseType{data1=Type1,data2=Type2}. var combinedPattern = regexp.MustCompile(`^([\w\-./\[\]]+){(.*)}$`) func (operation *Operation) parseObjectSchema(refType string, astFile *ast.File) (*spec.Schema, error) { + return parseObjectSchema(operation.parser, refType, astFile) +} + +func parseObjectSchema(parser *Parser, refType string, astFile *ast.File) (*spec.Schema, error) { switch { case refType == NIL: return nil, nil @@ -838,7 +847,7 @@ func (operation *Operation) parseObjectSchema(refType string, astFile *ast.File) case IsPrimitiveType(refType): return PrimitiveSchema(refType), nil case strings.HasPrefix(refType, "[]"): - schema, err := operation.parseObjectSchema(refType[2:], astFile) + schema, err := parseObjectSchema(parser, refType[2:], astFile) if err != nil { return nil, err } @@ -856,17 +865,17 @@ func (operation *Operation) parseObjectSchema(refType string, astFile *ast.File) return spec.MapProperty(nil), nil } - schema, err := operation.parseObjectSchema(refType, astFile) + schema, err := parseObjectSchema(parser, refType, astFile) if err != nil { return nil, err } return spec.MapProperty(schema), nil case strings.Contains(refType, "{"): - return operation.parseCombinedObjectSchema(refType, astFile) + return parseCombinedObjectSchema(parser, refType, astFile) default: - if operation.parser != nil { // checking refType has existing in 'TypeDefinitions' - schema, err := operation.parser.getTypeSchema(refType, astFile, true) + if parser != nil { // checking refType has existing in 'TypeDefinitions' + schema, err := parser.getTypeSchema(refType, astFile, true) if err != nil { return nil, err } @@ -896,13 +905,13 @@ func parseFields(s string) []string { }) } -func (operation *Operation) parseCombinedObjectSchema(refType string, astFile *ast.File) (*spec.Schema, error) { +func parseCombinedObjectSchema(parser *Parser, refType string, astFile *ast.File) (*spec.Schema, error) { matches := combinedPattern.FindStringSubmatch(refType) if len(matches) != 3 { return nil, fmt.Errorf("invalid type: %s", refType) } - schema, err := operation.parseObjectSchema(matches[1], astFile) + schema, err := parseObjectSchema(parser, matches[1], astFile) if err != nil { return nil, err } @@ -912,7 +921,7 @@ func (operation *Operation) parseCombinedObjectSchema(refType string, astFile *a for _, field := range fields { keyVal := strings.SplitN(field, "=", 2) if len(keyVal) == 2 { - schema, err := operation.parseObjectSchema(keyVal[1], astFile) + schema, err := parseObjectSchema(parser, keyVal[1], astFile) if err != nil { return nil, err } @@ -933,7 +942,16 @@ func (operation *Operation) parseCombinedObjectSchema(refType string, astFile *a }), nil } -func (operation *Operation) parseAPIObjectSchema(schemaType, refType string, astFile *ast.File) (*spec.Schema, error) { +func (operation *Operation) parseAPIObjectSchema(commentLine, schemaType, refType string, astFile *ast.File) (*spec.Schema, error) { + if strings.HasSuffix(refType, ",") && strings.Contains(refType, "[") { + // regexp may have broken generics syntax. find closing bracket and add it back + allMatchesLenOffset := strings.Index(commentLine, refType) + len(refType) + lostPartEndIdx := strings.Index(commentLine[allMatchesLenOffset:], "]") + if lostPartEndIdx >= 0 { + refType += commentLine[allMatchesLenOffset : allMatchesLenOffset+lostPartEndIdx+1] + } + } + switch schemaType { case OBJECT: if !strings.HasPrefix(refType, "[]") { @@ -969,7 +987,7 @@ func (operation *Operation) ParseResponseComment(commentLine string, astFile *as description := strings.Trim(matches[4], "\"") - schema, err := operation.parseAPIObjectSchema(strings.Trim(matches[2], "{}"), matches[3], astFile) + schema, err := operation.parseAPIObjectSchema(commentLine, strings.Trim(matches[2], "{}"), strings.TrimSpace(matches[3]), astFile) if err != nil { return err } @@ -1034,7 +1052,7 @@ func (operation *Operation) ParseResponseHeaderComment(commentLine string, _ *as header := newHeaderSpec(strings.Trim(matches[2], "{}"), strings.Trim(matches[4], "\"")) - headerKey := matches[3] + headerKey := strings.TrimSpace(matches[3]) if strings.EqualFold(matches[1], "all") { if operation.Responses.Default != nil { @@ -1121,7 +1139,7 @@ func (operation *Operation) ParseEmptyResponseOnly(commentLine string) error { return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } - operation.AddResponse(code, spec.NewResponse()) + operation.AddResponse(code, spec.NewResponse().WithDescription(http.StatusText(code))) } return nil @@ -1184,17 +1202,17 @@ func createParameter(paramType, description, paramName, schemaType string, requi } func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error) { - filesInfos, err := ioutil.ReadDir(dirPath) + dirEntries, err := os.ReadDir(dirPath) if err != nil { return nil, err } - for _, fileInfo := range filesInfos { - if fileInfo.IsDir() { + for _, entry := range dirEntries { + if entry.IsDir() { continue } - fileName := fileInfo.Name() + fileName := entry.Name() if !strings.Contains(fileName, ".json") { continue @@ -1203,7 +1221,7 @@ func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error if strings.Contains(fileName, summaryName) { fullPath := filepath.Join(dirPath, fileName) - commentInfo, err := ioutil.ReadFile(fullPath) + commentInfo, err := os.ReadFile(fullPath) if err != nil { return nil, fmt.Errorf("Failed to read code example file %s error: %s ", fullPath, err) } diff --git a/operation_test.go b/operation_test.go index d8089f0a2..d10f5da07 100644 --- a/operation_test.go +++ b/operation_test.go @@ -1071,7 +1071,7 @@ func TestParseEmptyResponseOnlyCode(t *testing.T) { expected := `{ "responses": { "200": { - "description": "" + "description": "OK" } } }` @@ -1091,10 +1091,10 @@ func TestParseEmptyResponseOnlyCodes(t *testing.T) { expected := `{ "responses": { "200": { - "description": "" + "description": "OK" }, "201": { - "description": "" + "description": "Created" }, "default": { "description": "" diff --git a/package.go b/package.go new file mode 100644 index 000000000..08f8b92b3 --- /dev/null +++ b/package.go @@ -0,0 +1,183 @@ +package swag + +import ( + "go/ast" + "go/token" + "reflect" + "strconv" +) + +// PackageDefinitions files and definition in a package. +type PackageDefinitions struct { + // 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 + + // const variables in this package, map key is the name + ConstTable map[string]*ConstVariable + + // const variables in order in this package + OrderedConst []*ConstVariable + + // package name + Name string + + // package path + Path string +} + +// ConstVariableGlobalEvaluator an interface used to evaluate enums across packages +type ConstVariableGlobalEvaluator interface { + EvaluateConstValue(pkg *PackageDefinitions, cv *ConstVariable, recursiveStack map[string]struct{}) (interface{}, ast.Expr) + EvaluateConstValueByName(file *ast.File, pkgPath, constVariableName string, recursiveStack map[string]struct{}) (interface{}, ast.Expr) + FindTypeSpec(typeName string, file *ast.File) *TypeSpecDef +} + +// NewPackageDefinitions new a PackageDefinitions object +func NewPackageDefinitions(name, pkgPath string) *PackageDefinitions { + return &PackageDefinitions{ + Name: name, + Path: pkgPath, + Files: make(map[string]*ast.File), + TypeDefinitions: make(map[string]*TypeSpecDef), + ConstTable: make(map[string]*ConstVariable), + } +} + +// AddFile add a file +func (pkg *PackageDefinitions) AddFile(pkgPath string, file *ast.File) *PackageDefinitions { + pkg.Files[pkgPath] = file + return pkg +} + +// AddTypeSpec add a type spec. +func (pkg *PackageDefinitions) AddTypeSpec(name string, typeSpec *TypeSpecDef) *PackageDefinitions { + pkg.TypeDefinitions[name] = typeSpec + return pkg +} + +// AddConst add a const variable. +func (pkg *PackageDefinitions) AddConst(astFile *ast.File, valueSpec *ast.ValueSpec) *PackageDefinitions { + for i := 0; i < len(valueSpec.Names) && i < len(valueSpec.Values); i++ { + variable := &ConstVariable{ + Name: valueSpec.Names[i], + Type: valueSpec.Type, + Value: valueSpec.Values[i], + Comment: valueSpec.Comment, + File: astFile, + } + pkg.ConstTable[valueSpec.Names[i].Name] = variable + pkg.OrderedConst = append(pkg.OrderedConst, variable) + } + return pkg +} + +func (pkg *PackageDefinitions) evaluateConstValue(file *ast.File, iota int, expr ast.Expr, globalEvaluator ConstVariableGlobalEvaluator, recursiveStack map[string]struct{}) (interface{}, ast.Expr) { + switch valueExpr := expr.(type) { + case *ast.Ident: + if valueExpr.Name == "iota" { + return iota, nil + } + if pkg.ConstTable != nil { + if cv, ok := pkg.ConstTable[valueExpr.Name]; ok { + return globalEvaluator.EvaluateConstValue(pkg, cv, recursiveStack) + } + } + case *ast.SelectorExpr: + pkgIdent, ok := valueExpr.X.(*ast.Ident) + if !ok { + return nil, nil + } + return globalEvaluator.EvaluateConstValueByName(file, pkgIdent.Name, valueExpr.Sel.Name, recursiveStack) + case *ast.BasicLit: + switch valueExpr.Kind { + case token.INT: + // hexadecimal + if len(valueExpr.Value) > 2 && valueExpr.Value[0] == '0' && valueExpr.Value[1] == 'x' { + if x, err := strconv.ParseInt(valueExpr.Value[2:], 16, 64); err == nil { + return int(x), nil + } else if x, err := strconv.ParseUint(valueExpr.Value[2:], 16, 64); err == nil { + return x, nil + } else { + panic(err) + } + } + + //octet + if len(valueExpr.Value) > 1 && valueExpr.Value[0] == '0' { + if x, err := strconv.ParseInt(valueExpr.Value[1:], 8, 64); err == nil { + return int(x), nil + } else if x, err := strconv.ParseUint(valueExpr.Value[1:], 8, 64); err == nil { + return x, nil + } else { + panic(err) + } + } + + //a basic literal integer is int type in default, or must have an explicit converting type in front + if x, err := strconv.ParseInt(valueExpr.Value, 10, 64); err == nil { + return int(x), nil + } else if x, err := strconv.ParseUint(valueExpr.Value, 10, 64); err == nil { + return x, nil + } else { + panic(err) + } + case token.STRING: + if valueExpr.Value[0] == '`' { + return valueExpr.Value[1 : len(valueExpr.Value)-1], nil + } + return EvaluateEscapedString(valueExpr.Value[1 : len(valueExpr.Value)-1]), nil + case token.CHAR: + return EvaluateEscapedChar(valueExpr.Value[1 : len(valueExpr.Value)-1]), nil + } + case *ast.UnaryExpr: + x, evalType := pkg.evaluateConstValue(file, iota, valueExpr.X, globalEvaluator, recursiveStack) + if x == nil { + return x, evalType + } + return EvaluateUnary(x, valueExpr.Op, evalType) + case *ast.BinaryExpr: + x, evalTypex := pkg.evaluateConstValue(file, iota, valueExpr.X, globalEvaluator, recursiveStack) + y, evalTypey := pkg.evaluateConstValue(file, iota, valueExpr.Y, globalEvaluator, recursiveStack) + if x == nil || y == nil { + return nil, nil + } + return EvaluateBinary(x, y, valueExpr.Op, evalTypex, evalTypey) + case *ast.ParenExpr: + return pkg.evaluateConstValue(file, iota, valueExpr.X, globalEvaluator, recursiveStack) + case *ast.CallExpr: + //data conversion + if len(valueExpr.Args) != 1 { + return nil, nil + } + arg := valueExpr.Args[0] + if ident, ok := valueExpr.Fun.(*ast.Ident); ok { + name := ident.Name + if name == "uintptr" { + name = "uint" + } + if IsGolangPrimitiveType(name) { + value, _ := pkg.evaluateConstValue(file, iota, arg, globalEvaluator, recursiveStack) + value = EvaluateDataConversion(value, name) + return value, nil + } else if name == "len" { + value, _ := pkg.evaluateConstValue(file, iota, arg, globalEvaluator, recursiveStack) + return reflect.ValueOf(value).Len(), nil + } + typeDef := globalEvaluator.FindTypeSpec(name, file) + if typeDef == nil { + return nil, nil + } + return arg, valueExpr.Fun + } else if selector, ok := valueExpr.Fun.(*ast.SelectorExpr); ok { + typeDef := globalEvaluator.FindTypeSpec(fullTypeName(selector.X.(*ast.Ident).Name, selector.Sel.Name), file) + if typeDef == nil { + return nil, nil + } + return arg, typeDef.TypeSpec.Type + } + } + return nil, nil +} diff --git a/packages.go b/packages.go index fc7a6bd2b..9480acb17 100644 --- a/packages.go +++ b/packages.go @@ -1,6 +1,7 @@ package swag import ( + "fmt" "go/ast" goparser "go/parser" "go/token" @@ -18,6 +19,8 @@ type PackagesDefinitions struct { files map[*ast.File]*AstFileInfo packages map[string]*PackageDefinitions uniqueDefinitions map[string]*TypeSpecDef + parseDependency bool + debug Debugger } // NewPackagesDefinitions create object PackagesDefinitions. @@ -29,8 +32,19 @@ func NewPackagesDefinitions() *PackagesDefinitions { } } -// CollectAstFile collect ast.file. -func (pkgDefs *PackagesDefinitions) CollectAstFile(packageDir, path string, astFile *ast.File) error { +// ParseFile parse a source file. +func (pkgDefs *PackagesDefinitions) ParseFile(packageDir, path string, src interface{}) error { + // positions are relative to FileSet + fileSet := token.NewFileSet() + astFile, err := goparser.ParseFile(fileSet, path, src, goparser.ParseComments) + if err != nil { + return fmt.Errorf("failed to parse file %s, error:%+v", path, err) + } + return pkgDefs.collectAstFile(fileSet, packageDir, path, astFile) +} + +// collectAstFile collect ast.file. +func (pkgDefs *PackagesDefinitions) collectAstFile(fileSet *token.FileSet, packageDir, path string, astFile *ast.File) error { if pkgDefs.files == nil { pkgDefs.files = make(map[*ast.File]*AstFileInfo) } @@ -59,14 +73,11 @@ func (pkgDefs *PackagesDefinitions) CollectAstFile(packageDir, path string, astF dependency.Files[path] = astFile } else { - pkgDefs.packages[packageDir] = &PackageDefinitions{ - Name: astFile.Name.Name, - Files: map[string]*ast.File{path: astFile}, - TypeDefinitions: make(map[string]*TypeSpecDef), - } + pkgDefs.packages[packageDir] = NewPackageDefinitions(astFile.Name.Name, packageDir).AddFile(path, astFile) } pkgDefs.files[astFile] = &AstFileInfo{ + FileSet: fileSet, File: astFile, Path: path, PackagePath: packageDir, @@ -76,9 +87,9 @@ func (pkgDefs *PackagesDefinitions) CollectAstFile(packageDir, path string, astF } // RangeFiles for range the collection of ast.File in alphabetic order. -func rangeFiles(files map[*ast.File]*AstFileInfo, handle func(filename string, file *ast.File) error) error { - sortedFiles := make([]*AstFileInfo, 0, len(files)) - for _, info := range files { +func (pkgDefs *PackagesDefinitions) RangeFiles(handle func(filename string, file *ast.File) error) error { + sortedFiles := make([]*AstFileInfo, 0, len(pkgDefs.files)) + for _, info := range pkgDefs.files { // ignore package path prefix with 'vendor' or $GOROOT, // because the router info of api will not be included these files. if strings.HasPrefix(info.PackagePath, "vendor") || strings.HasPrefix(info.Path, runtime.GOROOT()) { @@ -107,13 +118,21 @@ func (pkgDefs *PackagesDefinitions) ParseTypes() (map[*TypeSpecDef]*Schema, erro parsedSchemas := make(map[*TypeSpecDef]*Schema) for astFile, info := range pkgDefs.files { pkgDefs.parseTypesFromFile(astFile, info.PackagePath, parsedSchemas) + pkgDefs.parseFunctionScopedTypesFromFile(astFile, info.PackagePath, parsedSchemas) } + pkgDefs.removeAllNotUniqueTypes() + pkgDefs.evaluateAllConstVariables() + pkgDefs.collectConstEnums(parsedSchemas) return parsedSchemas, nil } func (pkgDefs *PackagesDefinitions) parseTypesFromFile(astFile *ast.File, packagePath string, parsedSchemas map[*TypeSpecDef]*Schema) { for _, astDeclaration := range astFile.Decls { - if generalDeclaration, ok := astDeclaration.(*ast.GenDecl); ok && generalDeclaration.Tok == token.TYPE { + generalDeclaration, ok := astDeclaration.(*ast.GenDecl) + if !ok { + continue + } + if generalDeclaration.Tok == token.TYPE { for _, astSpec := range generalDeclaration.Specs { if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { typeSpecDef := &TypeSpecDef{ @@ -134,25 +153,96 @@ func (pkgDefs *PackagesDefinitions) parseTypesFromFile(astFile *ast.File, packag pkgDefs.uniqueDefinitions = make(map[string]*TypeSpecDef) } - fullName := typeSpecDef.FullName() + fullName := typeSpecDef.TypeName() + anotherTypeDef, ok := pkgDefs.uniqueDefinitions[fullName] if ok { - if typeSpecDef.PkgPath == anotherTypeDef.PkgPath { - continue - } else { - delete(pkgDefs.uniqueDefinitions, fullName) + if anotherTypeDef == nil { + typeSpecDef.NotUnique = true + fullName = typeSpecDef.TypeName() + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } else if typeSpecDef.PkgPath != anotherTypeDef.PkgPath { + pkgDefs.uniqueDefinitions[fullName] = nil + anotherTypeDef.NotUnique = true + pkgDefs.uniqueDefinitions[anotherTypeDef.TypeName()] = anotherTypeDef + typeSpecDef.NotUnique = true + fullName = typeSpecDef.TypeName() + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef } } else { pkgDefs.uniqueDefinitions[fullName] = typeSpecDef } if pkgDefs.packages[typeSpecDef.PkgPath] == nil { - pkgDefs.packages[typeSpecDef.PkgPath] = &PackageDefinitions{ - Name: astFile.Name.Name, - TypeDefinitions: map[string]*TypeSpecDef{typeSpecDef.Name(): typeSpecDef}, - } + pkgDefs.packages[typeSpecDef.PkgPath] = NewPackageDefinitions(astFile.Name.Name, typeSpecDef.PkgPath).AddTypeSpec(typeSpecDef.Name(), typeSpecDef) } else if _, ok = pkgDefs.packages[typeSpecDef.PkgPath].TypeDefinitions[typeSpecDef.Name()]; !ok { - pkgDefs.packages[typeSpecDef.PkgPath].TypeDefinitions[typeSpecDef.Name()] = typeSpecDef + pkgDefs.packages[typeSpecDef.PkgPath].AddTypeSpec(typeSpecDef.Name(), typeSpecDef) + } + } + } + } else if generalDeclaration.Tok == token.CONST { + // collect consts + pkgDefs.collectConstVariables(astFile, packagePath, generalDeclaration) + } + } +} + +func (pkgDefs *PackagesDefinitions) parseFunctionScopedTypesFromFile(astFile *ast.File, packagePath string, parsedSchemas map[*TypeSpecDef]*Schema) { + for _, astDeclaration := range astFile.Decls { + funcDeclaration, ok := astDeclaration.(*ast.FuncDecl) + if ok && funcDeclaration.Body != nil { + for _, stmt := range funcDeclaration.Body.List { + if declStmt, ok := (stmt).(*ast.DeclStmt); ok { + if genDecl, ok := (declStmt.Decl).(*ast.GenDecl); ok && genDecl.Tok == token.TYPE { + for _, astSpec := range genDecl.Specs { + if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { + typeSpecDef := &TypeSpecDef{ + PkgPath: packagePath, + File: astFile, + TypeSpec: typeSpec, + ParentSpec: astDeclaration, + } + + if idt, ok := typeSpec.Type.(*ast.Ident); ok && IsGolangPrimitiveType(idt.Name) && parsedSchemas != nil { + parsedSchemas[typeSpecDef] = &Schema{ + PkgPath: typeSpecDef.PkgPath, + Name: astFile.Name.Name, + Schema: PrimitiveSchema(TransToValidSchemeType(idt.Name)), + } + } + + if pkgDefs.uniqueDefinitions == nil { + pkgDefs.uniqueDefinitions = make(map[string]*TypeSpecDef) + } + + fullName := typeSpecDef.TypeName() + + anotherTypeDef, ok := pkgDefs.uniqueDefinitions[fullName] + if ok { + if anotherTypeDef == nil { + typeSpecDef.NotUnique = true + fullName = typeSpecDef.TypeName() + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } else if typeSpecDef.PkgPath != anotherTypeDef.PkgPath { + pkgDefs.uniqueDefinitions[fullName] = nil + anotherTypeDef.NotUnique = true + pkgDefs.uniqueDefinitions[anotherTypeDef.TypeName()] = anotherTypeDef + typeSpecDef.NotUnique = true + fullName = typeSpecDef.TypeName() + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } + } else { + pkgDefs.uniqueDefinitions[fullName] = typeSpecDef + } + + if pkgDefs.packages[typeSpecDef.PkgPath] == nil { + pkgDefs.packages[typeSpecDef.PkgPath] = NewPackageDefinitions(astFile.Name.Name, typeSpecDef.PkgPath).AddTypeSpec(fullName, typeSpecDef) + } else if _, ok = pkgDefs.packages[typeSpecDef.PkgPath].TypeDefinitions[fullName]; !ok { + pkgDefs.packages[typeSpecDef.PkgPath].AddTypeSpec(fullName, typeSpecDef) + } + } + } + } } } @@ -160,6 +250,146 @@ func (pkgDefs *PackagesDefinitions) parseTypesFromFile(astFile *ast.File, packag } } +func (pkgDefs *PackagesDefinitions) collectConstVariables(astFile *ast.File, packagePath string, generalDeclaration *ast.GenDecl) { + pkg, ok := pkgDefs.packages[packagePath] + if !ok { + pkg = NewPackageDefinitions(astFile.Name.Name, packagePath) + pkgDefs.packages[packagePath] = pkg + } + + var lastValueSpec *ast.ValueSpec + for _, astSpec := range generalDeclaration.Specs { + valueSpec, ok := astSpec.(*ast.ValueSpec) + if !ok { + continue + } + if len(valueSpec.Names) == 1 && len(valueSpec.Values) == 1 { + lastValueSpec = valueSpec + } else if len(valueSpec.Names) == 1 && len(valueSpec.Values) == 0 && valueSpec.Type == nil && lastValueSpec != nil { + valueSpec.Type = lastValueSpec.Type + valueSpec.Values = lastValueSpec.Values + } + pkg.AddConst(astFile, valueSpec) + } +} + +func (pkgDefs *PackagesDefinitions) evaluateAllConstVariables() { + for _, pkg := range pkgDefs.packages { + for _, constVar := range pkg.OrderedConst { + pkgDefs.EvaluateConstValue(pkg, constVar, nil) + } + } +} + +// EvaluateConstValue evaluate a const variable. +func (pkgDefs *PackagesDefinitions) EvaluateConstValue(pkg *PackageDefinitions, cv *ConstVariable, recursiveStack map[string]struct{}) (interface{}, ast.Expr) { + if expr, ok := cv.Value.(ast.Expr); ok { + defer func() { + if err := recover(); err != nil { + if fi, ok := pkgDefs.files[cv.File]; ok { + pos := fi.FileSet.Position(cv.Name.NamePos) + pkgDefs.debug.Printf("warning: failed to evaluate const %s at %s:%d:%d, %v", cv.Name.Name, fi.Path, pos.Line, pos.Column, err) + } + } + }() + if recursiveStack == nil { + recursiveStack = make(map[string]struct{}) + } + fullConstName := fullTypeName(pkg.Path, cv.Name.Name) + if _, ok = recursiveStack[fullConstName]; ok { + return nil, nil + } + recursiveStack[fullConstName] = struct{}{} + + value, evalType := pkg.evaluateConstValue(cv.File, cv.Name.Obj.Data.(int), expr, pkgDefs, recursiveStack) + if cv.Type == nil && evalType != nil { + cv.Type = evalType + } + if value != nil { + cv.Value = value + } + return value, cv.Type + } + return cv.Value, cv.Type +} + +// EvaluateConstValueByName evaluate a const variable by name. +func (pkgDefs *PackagesDefinitions) EvaluateConstValueByName(file *ast.File, pkgName, constVariableName string, recursiveStack map[string]struct{}) (interface{}, ast.Expr) { + matchedPkgPaths, externalPkgPaths := pkgDefs.findPackagePathFromImports(pkgName, file) + for _, pkgPath := range matchedPkgPaths { + if pkg, ok := pkgDefs.packages[pkgPath]; ok { + if cv, ok := pkg.ConstTable[constVariableName]; ok { + return pkgDefs.EvaluateConstValue(pkg, cv, recursiveStack) + } + } + } + if pkgDefs.parseDependency { + for _, pkgPath := range externalPkgPaths { + if err := pkgDefs.loadExternalPackage(pkgPath); err == nil { + if pkg, ok := pkgDefs.packages[pkgPath]; ok { + if cv, ok := pkg.ConstTable[constVariableName]; ok { + return pkgDefs.EvaluateConstValue(pkg, cv, recursiveStack) + } + } + } + } + } + return nil, nil +} + +func (pkgDefs *PackagesDefinitions) collectConstEnums(parsedSchemas map[*TypeSpecDef]*Schema) { + for _, pkg := range pkgDefs.packages { + for _, constVar := range pkg.OrderedConst { + if constVar.Type == nil { + continue + } + ident, ok := constVar.Type.(*ast.Ident) + if !ok || IsGolangPrimitiveType(ident.Name) { + continue + } + typeDef, ok := pkg.TypeDefinitions[ident.Name] + if !ok { + continue + } + + //delete it from parsed schemas, and will parse it again + if _, ok := parsedSchemas[typeDef]; ok { + delete(parsedSchemas, typeDef) + } + + if typeDef.Enums == nil { + typeDef.Enums = make([]EnumValue, 0) + } + + name := constVar.Name.Name + if _, ok := constVar.Value.(ast.Expr); ok { + continue + } + + enumValue := EnumValue{ + key: name, + Value: constVar.Value, + } + if constVar.Comment != nil && len(constVar.Comment.List) > 0 { + enumValue.Comment = constVar.Comment.List[0].Text + enumValue.Comment = strings.TrimLeft(enumValue.Comment, "//") + enumValue.Comment = strings.TrimLeft(enumValue.Comment, "/*") + enumValue.Comment = strings.TrimRight(enumValue.Comment, "*/") + enumValue.Comment = strings.TrimSpace(enumValue.Comment) + } + typeDef.Enums = append(typeDef.Enums, enumValue) + } + } +} + +func (pkgDefs *PackagesDefinitions) removeAllNotUniqueTypes() { + for key, ud := range pkgDefs.uniqueDefinitions { + if ud == nil { + delete(pkgDefs.uniqueDefinitions, key) + } + } +} + func (pkgDefs *PackagesDefinitions) findTypeSpec(pkgPath string, typeName string) *TypeSpecDef { if pkgDefs.packages == nil { return nil @@ -207,83 +437,100 @@ func (pkgDefs *PackagesDefinitions) loadExternalPackage(importPath string) error // findPackagePathFromImports finds out the package path of a package via ranging imports of an ast.File // @pkg the name of the target package // @file current ast.File in which to search imports -// @fuzzy search for the package path that the last part matches the @pkg if true -// @return the package path of a package of @pkg. -func (pkgDefs *PackagesDefinitions) findPackagePathFromImports(pkg string, file *ast.File, fuzzy bool) string { +// @return the package paths of a package of @pkg. +func (pkgDefs *PackagesDefinitions) findPackagePathFromImports(pkg string, file *ast.File) (matchedPkgPaths, externalPkgPaths []string) { if file == nil { - return "" + return } if strings.ContainsRune(pkg, '.') { pkg = strings.Split(pkg, ".")[0] } - hasAnonymousPkg := false - matchLastPathPart := func(pkgPath string) bool { paths := strings.Split(pkgPath, "/") - return paths[len(paths)-1] == pkg } // prior to match named package for _, imp := range file.Imports { + path := strings.Trim(imp.Path.Value, `"`) if imp.Name != nil { if imp.Name.Name == pkg { - return strings.Trim(imp.Path.Value, `"`) + // if name match, break loop and return + _, ok := pkgDefs.packages[path] + if ok { + matchedPkgPaths = []string{path} + externalPkgPaths = nil + } else { + externalPkgPaths = []string{path} + matchedPkgPaths = nil + } + break + } else if imp.Name.Name == "_" && len(pkg) > 0 { + //for unused types + pd, ok := pkgDefs.packages[path] + if ok { + if pd.Name == pkg { + matchedPkgPaths = append(matchedPkgPaths, path) + } + } else if matchLastPathPart(path) { + externalPkgPaths = append(externalPkgPaths, path) + } + } else if imp.Name.Name == "." && len(pkg) == 0 { + _, ok := pkgDefs.packages[path] + if ok { + matchedPkgPaths = append(matchedPkgPaths, path) + } else if len(pkg) == 0 || matchLastPathPart(path) { + externalPkgPaths = append(externalPkgPaths, path) + } } - - if imp.Name.Name == "_" { - hasAnonymousPkg = true + } else if pkgDefs.packages != nil && len(pkg) > 0 { + pd, ok := pkgDefs.packages[path] + if ok { + if pd.Name == pkg { + matchedPkgPaths = append(matchedPkgPaths, path) + } + } else if matchLastPathPart(path) { + externalPkgPaths = append(externalPkgPaths, path) } - - continue } + } - if pkgDefs.packages != nil { - path := strings.Trim(imp.Path.Value, `"`) - if fuzzy { - if matchLastPathPart(path) { - return path - } + if len(pkg) == 0 || file.Name.Name == pkg { + matchedPkgPaths = append(matchedPkgPaths, pkgDefs.files[file].PackagePath) + } - continue - } + return +} - pd, ok := pkgDefs.packages[path] - if ok && pd.Name == pkg { - return path - } +func (pkgDefs *PackagesDefinitions) findTypeSpecFromPackagePaths(matchedPkgPaths, externalPkgPaths []string, name string) (typeDef *TypeSpecDef) { + for _, pkgPath := range matchedPkgPaths { + typeDef = pkgDefs.findTypeSpec(pkgPath, name) + if typeDef != nil { + return typeDef } } - // match unnamed package - if hasAnonymousPkg && pkgDefs.packages != nil { - for _, imp := range file.Imports { - if imp.Name == nil { - continue - } - if imp.Name.Name == "_" { - path := strings.Trim(imp.Path.Value, `"`) - if fuzzy { - if matchLastPathPart(path) { - return path - } - } else if pd, ok := pkgDefs.packages[path]; ok && pd.Name == pkg { - return path + if pkgDefs.parseDependency { + for _, pkgPath := range externalPkgPaths { + if err := pkgDefs.loadExternalPackage(pkgPath); err == nil { + typeDef = pkgDefs.findTypeSpec(pkgPath, name) + if typeDef != nil { + return typeDef } } } } - return "" + return typeDef } // 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 (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File, parseDependency bool) *TypeSpecDef { +func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File) *TypeSpecDef { if IsGolangPrimitiveType(typeName) { return nil } @@ -292,43 +539,16 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File return pkgDefs.uniqueDefinitions[typeName] } - parts := strings.Split(typeName, ".") + parts := strings.Split(strings.Split(typeName, "[")[0], ".") if len(parts) > 1 { - 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]) { - typeDef, ok := pkgDefs.uniqueDefinitions[typeName] - if ok { - return typeDef - } - } - - pkgPath := pkgDefs.findPackagePathFromImports(parts[0], file, false) - if len(pkgPath) == 0 { - // check if the current package - if parts[0] == file.Name.Name { - pkgPath = pkgDefs.files[file].PackagePath - } else if parseDependency { - // take it as an external package, needs to be loaded - if pkgPath = pkgDefs.findPackagePathFromImports(parts[0], file, true); len(pkgPath) > 0 { - if err := pkgDefs.loadExternalPackage(pkgPath); err != nil { - return nil - } - } - } + typeDef, ok := pkgDefs.uniqueDefinitions[typeName] + if ok { + return typeDef } - return pkgDefs.findTypeSpec(pkgPath, parts[1]) + pkgPaths, externalPkgPaths := pkgDefs.findPackagePathFromImports(parts[0], file) + typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, parts[1]) + return pkgDefs.parametrizeGenericType(file, typeDef, typeName) } typeDef, ok := pkgDefs.uniqueDefinitions[fullTypeName(file.Name.Name, typeName)] @@ -336,19 +556,17 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File return typeDef } - typeDef = pkgDefs.findTypeSpec(pkgDefs.files[file].PackagePath, typeName) - if typeDef != nil { + //in case that comment //@name renamed the type with a name without a dot + typeDef, ok = pkgDefs.uniqueDefinitions[typeName] + if ok { return typeDef } - for _, imp := range file.Imports { - if imp.Name != nil && imp.Name.Name == "." { - typeDef := pkgDefs.findTypeSpec(strings.Trim(imp.Path.Value, `"`), typeName) - if typeDef != nil { - return typeDef - } - } + name := parts[0] + typeDef, ok = pkgDefs.uniqueDefinitions[fullTypeName(file.Name.Name, name)] + if !ok { + pkgPaths, externalPkgPaths := pkgDefs.findPackagePathFromImports("", file) + typeDef = pkgDefs.findTypeSpecFromPackagePaths(pkgPaths, externalPkgPaths, name) } - - return nil + return pkgDefs.parametrizeGenericType(file, typeDef, typeName) } diff --git a/packages_test.go b/packages_test.go index d74ba4d3a..60beca2f8 100644 --- a/packages_test.go +++ b/packages_test.go @@ -10,20 +10,30 @@ import ( "github.com/stretchr/testify/assert" ) -func TestPackagesDefinitions_CollectAstFile(t *testing.T) { +func TestPackagesDefinitions_ParseFile(t *testing.T) { pd := PackagesDefinitions{} - assert.NoError(t, pd.CollectAstFile("", "", nil)) + packageDir := "github.com/swaggo/swag/testdata/simple" + assert.NoError(t, pd.ParseFile(packageDir, "testdata/simple/main.go", nil)) + assert.Equal(t, 1, len(pd.packages)) + assert.Equal(t, 1, len(pd.files)) +} + +func TestPackagesDefinitions_collectAstFile(t *testing.T) { + pd := PackagesDefinitions{} + fileSet := token.NewFileSet() + assert.NoError(t, pd.collectAstFile(fileSet, "", "", nil)) firstFile := &ast.File{ Name: &ast.Ident{Name: "main.go"}, } packageDir := "github.com/swaggo/swag/testdata/simple" - assert.NoError(t, pd.CollectAstFile(packageDir, "testdata/simple/"+firstFile.Name.String(), firstFile)) + assert.NoError(t, pd.collectAstFile(fileSet, packageDir, "testdata/simple/"+firstFile.Name.String(), firstFile)) assert.NotEmpty(t, pd.packages[packageDir]) absPath, _ := filepath.Abs("testdata/simple/" + firstFile.Name.String()) astFileInfo := &AstFileInfo{ + FileSet: fileSet, File: firstFile, Path: absPath, PackagePath: packageDir, @@ -31,14 +41,14 @@ func TestPackagesDefinitions_CollectAstFile(t *testing.T) { assert.Equal(t, pd.files[firstFile], astFileInfo) // Override - assert.NoError(t, pd.CollectAstFile(packageDir, "testdata/simple/"+firstFile.Name.String(), firstFile)) + assert.NoError(t, pd.collectAstFile(fileSet, packageDir, "testdata/simple/"+firstFile.Name.String(), firstFile)) assert.Equal(t, pd.files[firstFile], astFileInfo) // Another file secondFile := &ast.File{ Name: &ast.Ident{Name: "api.go"}, } - assert.NoError(t, pd.CollectAstFile(packageDir, "testdata/simple/"+secondFile.Name.String(), secondFile)) + assert.NoError(t, pd.collectAstFile(fileSet, packageDir, "testdata/simple/"+secondFile.Name.String(), secondFile)) } func TestPackagesDefinitions_rangeFiles(t *testing.T) { @@ -62,7 +72,7 @@ func TestPackagesDefinitions_rangeFiles(t *testing.T) { } i, expect := 0, []string{"testdata/simple/api/api.go", "testdata/simple/main.go"} - _ = rangeFiles(pd.files, func(filename string, file *ast.File) error { + _ = pd.RangeFiles(func(filename string, file *ast.File) error { assert.Equal(t, expect[i], filename) i++ return nil @@ -111,6 +121,51 @@ func TestPackagesDefinitions_ParseTypes(t *testing.T) { assert.NoError(t, err) } +func TestPackagesDefinitions_parseFunctionScopedTypesFromFile(t *testing.T) { + mainAST := &ast.File{ + Name: &ast.Ident{Name: "main.go"}, + Decls: []ast.Decl{ + &ast.FuncDecl{ + Name: ast.NewIdent("TestFuncDecl"), + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.DeclStmt{ + Decl: &ast.GenDecl{ + Tok: token.TYPE, + Specs: []ast.Spec{ + &ast.TypeSpec{ + Name: ast.NewIdent("response"), + Type: ast.NewIdent("struct"), + }, + &ast.TypeSpec{ + Name: ast.NewIdent("stringResponse"), + Type: ast.NewIdent("string"), + }, + }, + }, + }, + }, + }, + }, + }, + } + + pd := PackagesDefinitions{ + packages: make(map[string]*PackageDefinitions), + } + + parsedSchema := make(map[*TypeSpecDef]*Schema) + pd.parseFunctionScopedTypesFromFile(mainAST, "main", parsedSchema) + + assert.Len(t, parsedSchema, 1) + + _, ok := pd.uniqueDefinitions["main.go.TestFuncDecl.response"] + assert.True(t, ok) + + _, ok = pd.packages["main"].TypeDefinitions["main.go.TestFuncDecl.response"] + assert.True(t, ok) +} + func TestPackagesDefinitions_FindTypeSpec(t *testing.T) { userDef := TypeSpecDef{ File: &ast.File{ @@ -128,16 +183,17 @@ func TestPackagesDefinitions_FindTypeSpec(t *testing.T) { } var nilDef *TypeSpecDef - assert.Equal(t, nilDef, pkg.FindTypeSpec("int", nil, false)) - assert.Equal(t, nilDef, pkg.FindTypeSpec("bool", nil, false)) - assert.Equal(t, nilDef, pkg.FindTypeSpec("string", nil, false)) + assert.Equal(t, nilDef, pkg.FindTypeSpec("int", nil)) + assert.Equal(t, nilDef, pkg.FindTypeSpec("bool", nil)) + assert.Equal(t, nilDef, pkg.FindTypeSpec("string", nil)) - assert.Equal(t, &userDef, pkg.FindTypeSpec("user.Model", nil, false)) - assert.Equal(t, nilDef, pkg.FindTypeSpec("Model", nil, false)) + assert.Equal(t, &userDef, pkg.FindTypeSpec("user.Model", nil)) + assert.Equal(t, nilDef, pkg.FindTypeSpec("Model", nil)) } func TestPackage_rangeFiles(t *testing.T) { - files := map[*ast.File]*AstFileInfo{ + pd := NewPackagesDefinitions() + pd.files = map[*ast.File]*AstFileInfo{ { Name: &ast.Ident{Name: "main.go"}, }: { @@ -173,10 +229,10 @@ func TestPackage_rangeFiles(t *testing.T) { sorted = append(sorted, filename) return nil } - assert.NoError(t, rangeFiles(files, processor)) + assert.NoError(t, pd.RangeFiles(processor)) assert.Equal(t, []string{"testdata/simple/api/api.go", "testdata/simple/main.go"}, sorted) - assert.Error(t, rangeFiles(files, func(filename string, file *ast.File) error { + assert.Error(t, pd.RangeFiles(func(filename string, file *ast.File) error { return ErrFuncTypeField })) diff --git a/parser.go b/parser.go index 79966a302..ed2294d08 100644 --- a/parser.go +++ b/parser.go @@ -9,10 +9,8 @@ import ( "go/build" goparser "go/parser" "go/token" - "io/ioutil" "log" "net/http" - "net/url" "os" "os/exec" "path/filepath" @@ -106,15 +104,6 @@ type Parser struct { // outputSchemas store schemas which will be export to swagger outputSchemas map[*TypeSpecDef]*Schema - // existSchemaNames store names of models for conflict determination - existSchemaNames map[string]*Schema - - // toBeRenamedSchemas names of models to be renamed - toBeRenamedSchemas map[string]string - - // toBeRenamedSchemas URLs of ref models to be renamed - toBeRenamedRefURLs []*url.URL - // PropNamingStrategy naming strategy PropNamingStrategy string @@ -130,6 +119,9 @@ type Parser struct { // Strict whether swag should error or warn when it detects cases which are most likely user errors Strict bool + // RequiredByDefault set validation required for all fields by default + RequiredByDefault bool + // structStack stores full names of the structures that were already parsed or are being parsed now structStack []*TypeSpecDef @@ -159,6 +151,9 @@ type Parser struct { // parseGoList whether swag use go list to parse dependency parseGoList bool + + // tags to filter the APIs after + tags map[string]struct{} } // FieldParserFactory create FieldParser. @@ -209,9 +204,8 @@ func New(options ...func(*Parser)) *Parser { debug: log.New(os.Stdout, "", log.LstdFlags), parsedSchemas: make(map[*TypeSpecDef]*Schema), outputSchemas: make(map[*TypeSpecDef]*Schema), - existSchemaNames: make(map[string]*Schema), - toBeRenamedSchemas: make(map[string]string), excludes: make(map[string]struct{}), + tags: make(map[string]struct{}), fieldParserFactory: newTagBaseFieldParser, Overrides: make(map[string]string), } @@ -220,9 +214,21 @@ func New(options ...func(*Parser)) *Parser { option(parser) } + parser.packages.debug = parser.debug + return parser } +// SetParseDependency sets whether to parse the dependent packages. +func SetParseDependency(parseDependency bool) func(*Parser) { + return func(p *Parser) { + p.ParseDependency = parseDependency + if p.packages != nil { + p.packages.parseDependency = parseDependency + } + } +} + // SetMarkdownFileDirectory sets the directory to search for markdown files. func SetMarkdownFileDirectory(directoryPath string) func(*Parser) { return func(p *Parser) { @@ -250,6 +256,18 @@ func SetExcludedDirsAndFiles(excludes string) func(*Parser) { } } +// SetTags sets the tags to be included +func SetTags(include string) func(*Parser) { + return func(p *Parser) { + for _, f := range strings.Split(include, ",") { + f = strings.TrimSpace(f) + if f != "" { + p.tags[f] = struct{}{} + } + } + } +} + // SetParseExtension parses only those operations which match given extension func SetParseExtension(parseExtension string) func(*Parser) { return func(p *Parser) { @@ -270,7 +288,6 @@ func SetDebugger(logger Debugger) func(parser *Parser) { if logger != nil { p.debug = logger } - } } @@ -371,13 +388,11 @@ func (parser *Parser) ParseAPIMultiSearchDir(searchDirs []string, mainAPIFile st return err } - err = rangeFiles(parser.packages.files, parser.ParseRouterAPIInfo) + err = parser.packages.RangeFiles(parser.ParseRouterAPIInfo) if err != nil { return err } - parser.renameRefSchemas() - return parser.checkOperationIDUniqueness() } @@ -437,19 +452,23 @@ func parseGeneralAPIInfo(parser *Parser, comments []string) error { // parsing classic meta data model for line := 0; line < len(comments); line++ { commentLine := comments[line] - attribute := strings.Split(commentLine, " ")[0] - value := strings.TrimSpace(commentLine[len(attribute):]) + commentLine = strings.TrimSpace(commentLine) + if len(commentLine) == 0 { + continue + } + fields := FieldsByAnySpace(commentLine, 2) - multilineBlock := false - if previousAttribute == attribute { - multilineBlock = true + attribute := fields[0] + var value string + if len(fields) > 1 { + value = fields[1] } switch attr := strings.ToLower(attribute); attr { case versionAttr, titleAttr, tosAttr, licNameAttr, licURLAttr, conNameAttr, conURLAttr, conEmailAttr: setSwaggerInfo(parser.swagger, attr, value) case descriptionAttr: - if multilineBlock { + if previousAttribute == attribute { parser.swagger.Info.Description += "\n" + value continue @@ -529,14 +548,14 @@ func parseGeneralAPIInfo(parser *Parser, comments []string) error { case "@query.collection.format": parser.collectionFormatInQuery = value default: - prefixExtension := "@x-" - // Prefix extension + 1 char + 1 space + 1 char - if len(attribute) > 5 && attribute[:len(prefixExtension)] == prefixExtension { + if strings.HasPrefix(attribute, "@x-") { + extensionName := attribute[1:] + extExistsInSecurityDef := false // for each security definition for _, v := range parser.swagger.SecurityDefinitions { // check if extension exists - _, extExistsInSecurityDef = v.VendorExtensible.Extensions.GetString(attribute[1:]) + _, extExistsInSecurityDef = v.VendorExtensible.Extensions.GetString(extensionName) // if it exists in at least one, then we stop iterating if extExistsInSecurityDef { break @@ -548,16 +567,12 @@ func parseGeneralAPIInfo(parser *Parser, comments []string) error { break } - var valueJSON interface{} - - split := strings.SplitAfter(commentLine, attribute+" ") - if len(split) < 2 { + if len(value) == 0 { return fmt.Errorf("annotation %s need a value", attribute) } - extensionName := "x-" + strings.SplitAfter(attribute, prefixExtension)[1] - - err := json.Unmarshal([]byte(split[1]), &valueJSON) + var valueJSON interface{} + err := json.Unmarshal([]byte(value), &valueJSON) if err != nil { return fmt.Errorf("annotation %s need a valid json value", attribute) } @@ -728,7 +743,11 @@ func (parser *Parser) ParseProduceComment(commentLine string) error { func isGeneralAPIComment(comments []string) bool { for _, commentLine := range comments { - attribute := strings.ToLower(strings.Split(commentLine, " ")[0]) + commentLine = strings.TrimSpace(commentLine) + if len(commentLine) == 0 { + continue + } + attribute := strings.ToLower(FieldsByAnySpace(commentLine, 2)[0]) switch attribute { // The @summary, @router, @success, @failure annotation belongs to Operation case summaryAttr, routerAttr, successAttr, failureAttr, responseAttr: @@ -740,17 +759,17 @@ func isGeneralAPIComment(comments []string) bool { } func getMarkdownForTag(tagName string, dirPath string) ([]byte, error) { - filesInfos, err := ioutil.ReadDir(dirPath) + dirEntries, err := os.ReadDir(dirPath) if err != nil { return nil, err } - for _, fileInfo := range filesInfos { - if fileInfo.IsDir() { + for _, entry := range dirEntries { + if entry.IsDir() { continue } - fileName := fileInfo.Name() + fileName := entry.Name() if !strings.Contains(fileName, ".md") { continue @@ -759,7 +778,7 @@ func getMarkdownForTag(tagName string, dirPath string) ([]byte, error) { if strings.Contains(fileName, tagName) { fullPath := filepath.Join(dirPath, fileName) - commentInfo, err := ioutil.ReadFile(fullPath) + commentInfo, err := os.ReadFile(fullPath) if err != nil { return nil, fmt.Errorf("Failed to read markdown file %s error: %s ", fullPath, err) } @@ -784,24 +803,60 @@ func isExistsScope(scope string) (bool, error) { return strings.Contains(scope, scopeAttrPrefix), nil } +func getTagsFromComment(comment string) (tags []string) { + commentLine := strings.TrimSpace(strings.TrimLeft(comment, "/")) + if len(commentLine) == 0 { + return nil + } + + attribute := strings.Fields(commentLine)[0] + lineRemainder, lowerAttribute := strings.TrimSpace(commentLine[len(attribute):]), strings.ToLower(attribute) + + if lowerAttribute == tagsAttr { + for _, tag := range strings.Split(lineRemainder, ",") { + tags = append(tags, strings.TrimSpace(tag)) + } + } + return + +} + +func (parser *Parser) matchTags(comments []*ast.Comment) (match bool) { + if len(parser.tags) != 0 { + for _, comment := range comments { + for _, tag := range getTagsFromComment(comment.Text) { + if _, has := parser.tags["!"+tag]; has { + return false + } + if _, has := parser.tags[tag]; has { + match = true // keep iterating as it may contain a tag that is excluded + } + } + } + return + } + return true +} + // ParseRouterAPIInfo parses router api info for given astFile. func (parser *Parser) ParseRouterAPIInfo(fileName string, astFile *ast.File) error { for _, astDescription := range astFile.Decls { astDeclaration, ok := astDescription.(*ast.FuncDecl) if ok && astDeclaration.Doc != nil && astDeclaration.Doc.List != nil { - // for per 'function' comment, create a new 'Operation' object - operation := NewOperation(parser, SetCodeExampleFilesDirectory(parser.codeExampleFilesDir)) - for _, comment := range astDeclaration.Doc.List { - err := operation.ParseComment(comment.Text, astFile) + if parser.matchTags(astDeclaration.Doc.List) { + // for per 'function' comment, create a new 'Operation' object + operation := NewOperation(parser, SetCodeExampleFilesDirectory(parser.codeExampleFilesDir)) + for _, comment := range astDeclaration.Doc.List { + err := operation.ParseComment(comment.Text, astFile) + if err != nil { + return fmt.Errorf("ParseComment error in file %s :%+v", fileName, err) + } + } + err := processRouterOperation(parser, operation) if err != nil { - return fmt.Errorf("ParseComment error in file %s :%+v", fileName, err) + return err } } - - err := processRouterOperation(parser, operation) - if err != nil { - return err - } } } @@ -886,6 +941,14 @@ func convertFromSpecificToPrimitive(typeName string) (string, error) { } func (parser *Parser) getTypeSchema(typeName string, file *ast.File, ref bool) (*spec.Schema, error) { + if override, ok := parser.Overrides[typeName]; ok { + parser.debug.Printf("Override detected for %s: using %s instead", typeName, override) + return parseObjectSchema(parser, override, file) + } + + if IsInterfaceLike(typeName) { + return &spec.Schema{}, nil + } if IsGolangPrimitiveType(typeName) { return PrimitiveSchema(TransToValidSchemeType(typeName)), nil } @@ -895,7 +958,7 @@ func (parser *Parser) getTypeSchema(typeName string, file *ast.File, ref bool) ( return PrimitiveSchema(schemaType), nil } - typeSpecDef := parser.packages.FindTypeSpec(typeName, file, parser.ParseDependency) + typeSpecDef := parser.packages.FindTypeSpec(typeName, file) if typeSpecDef == nil { return nil, fmt.Errorf("cannot find type definition: %s", typeName) } @@ -929,69 +992,25 @@ func (parser *Parser) getTypeSchema(typeName string, file *ast.File, ref bool) ( if err == ErrRecursiveParseStruct && ref { return parser.getRefTypeSchema(typeSpecDef, schema), nil } - return nil, err } } - if ref && len(schema.Schema.Type) > 0 && schema.Schema.Type[0] == OBJECT { - return parser.getRefTypeSchema(typeSpecDef, schema), nil - } - - return schema.Schema, nil -} - -func (parser *Parser) renameRefSchemas() { - if len(parser.toBeRenamedSchemas) == 0 { - return - } - - // 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 - } - } - - // rename URLs if match - for _, refURL := range parser.toBeRenamedRefURLs { - parts := strings.Split(refURL.Fragment, "/") - name := parts[len(parts)-1] - - if pkgPath, ok := parser.toBeRenamedSchemas[name]; ok { - parts[len(parts)-1] = parser.renameSchema(name, pkgPath) - - refURL.Fragment = strings.Join(parts, "/") + if ref { + if IsComplexSchema(schema.Schema) { + return parser.getRefTypeSchema(typeSpecDef, schema), nil } + // if it is a simple schema, just return a copy + newSchema := *schema.Schema + return &newSchema, nil } -} - -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 + return schema.Schema, nil } func (parser *Parser) getRefTypeSchema(typeSpecDef *TypeSpecDef, schema *Schema) *spec.Schema { _, ok := parser.outputSchemas[typeSpecDef] if !ok { - existSchema, ok := parser.existSchemaNames[schema.Name] - if ok { - // store the first one to be renamed after parsing over - _, ok = parser.toBeRenamedSchemas[existSchema.Name] - if !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 - } - parser.swagger.Definitions[schema.Name] = spec.Schema{} if schema.Schema != nil { @@ -1002,8 +1021,6 @@ func (parser *Parser) getRefTypeSchema(typeSpecDef *TypeSpecDef, schema *Schema) } refSchema := RefSchema(schema.Name) - // store every URL - parser.toBeRenamedRefURLs = append(parser.toBeRenamedRefURLs, refSchema.Ref.GetURL()) return refSchema } @@ -1022,9 +1039,7 @@ func (parser *Parser) isInStructStack(typeSpecDef *TypeSpecDef) bool { // given name and package, and populates swagger schema definitions registry // with a schema for the given type func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) { - typeName := typeSpecDef.FullName() - refTypeName := TypeDocName(typeName, typeSpecDef.TypeSpec) - + typeName := typeSpecDef.TypeName() schema, found := parser.parsedSchemas[typeSpecDef] if found { parser.debug.Printf("Skipping '%s', already parsed.", typeName) @@ -1036,7 +1051,7 @@ func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) parser.debug.Printf("Skipping '%s', recursion detected.", typeName) return &Schema{ - Name: refTypeName, + Name: typeName, PkgPath: typeSpecDef.PkgPath, Schema: PrimitiveSchema(OBJECT), }, @@ -1056,8 +1071,27 @@ func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) fillDefinitionDescription(definition, typeSpecDef.File, typeSpecDef) } + if len(typeSpecDef.Enums) > 0 { + var varnames []string + var enumComments = make(map[string]string) + for _, value := range typeSpecDef.Enums { + definition.Enum = append(definition.Enum, value.Value) + varnames = append(varnames, value.key) + if len(value.Comment) > 0 { + enumComments[value.key] = value.Comment + } + } + if definition.Extensions == nil { + definition.Extensions = make(spec.Extensions) + } + definition.Extensions[enumVarNamesExtension] = varnames + if len(enumComments) > 0 { + definition.Extensions[enumCommentsExtension] = enumComments + } + } + sch := Schema{ - Name: refTypeName, + Name: typeName, PkgPath: typeSpecDef.PkgPath, Schema: definition, } @@ -1072,12 +1106,8 @@ func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) return &sch, nil } -func fullTypeName(pkgName, typeName string) string { - if pkgName != "" { - return pkgName + "." + typeName - } - - return typeName +func fullTypeName(parts ...string) string { + return strings.Join(parts, ".") } // fillDefinitionDescription additionally fills fields in definition (spec.Schema) @@ -1115,7 +1145,10 @@ func extractDeclarationDescription(commentGroups ...*ast.CommentGroup) string { for _, comment := range commentGroup.List { commentText := strings.TrimSpace(strings.TrimLeft(comment.Text, "/")) - attribute := strings.Split(commentText, " ")[0] + if len(commentText) == 0 { + continue + } + attribute := FieldsByAnySpace(commentText, 2)[0] if strings.ToLower(attribute) != descriptionAttr { if !isHandlingDescription { @@ -1180,12 +1213,10 @@ func (parser *Parser) parseTypeExpr(file *ast.File, typeExpr ast.Expr, ref bool) case *ast.FuncType: return nil, ErrFuncTypeField - // ... - default: - parser.debug.Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) + // ... } - return PrimitiveSchema(OBJECT), nil + return parser.parseGenericTypeExpr(file, typeExpr) } func (parser *Parser) parseStruct(file *ast.File, fields *ast.FieldList) (*spec.Schema, error) { @@ -1224,15 +1255,26 @@ func (parser *Parser) parseStruct(file *ast.File, fields *ast.FieldList) (*spec. } func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[string]spec.Schema, []string, error) { - if field.Names == 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 - } + if field.Tag != nil { + skip, ok := reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", "")).Lookup("swaggerignore") + if ok && strings.EqualFold(skip, "true") { + return nil, nil, nil } + } + + ps := parser.fieldParserFactory(parser, field) + + if ps.ShouldSkip() { + return nil, nil, nil + } + + fieldName, err := ps.FieldName() + if err != nil { + return nil, nil, err + } - typeName, err := getFieldType(field.Type) + if fieldName == "" { + typeName, err := getFieldType(file, field.Type) if err != nil { return nil, nil, err } @@ -1254,20 +1296,9 @@ func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[st return properties, schema.SchemaProps.Required, 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 - } - - ps := parser.fieldParserFactory(parser, field) - if ps.ShouldSkip() { - return nil, nil, nil - } - - fieldName, err := ps.FieldName() - if err != nil { - return nil, nil, err } schema, err := ps.CustomSchema() @@ -1276,7 +1307,7 @@ func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[st } if schema == nil { - typeName, err := getFieldType(field.Type) + typeName, err := getFieldType(file, field.Type) if err == nil { // named type schema, err = parser.getTypeSchema(typeName, file, true) @@ -1309,26 +1340,26 @@ func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[st return map[string]spec.Schema{fieldName: *schema}, tagRequired, nil } -func getFieldType(field ast.Expr) (string, error) { +func getFieldType(file *ast.File, field ast.Expr) (string, error) { switch fieldType := field.(type) { case *ast.Ident: return fieldType.Name, nil case *ast.SelectorExpr: - packageName, err := getFieldType(fieldType.X) + packageName, err := getFieldType(file, fieldType.X) if err != nil { return "", err } return fullTypeName(packageName, fieldType.Sel.Name), nil case *ast.StarExpr: - fullName, err := getFieldType(fieldType.X) + fullName, err := getFieldType(file, fieldType.X) if err != nil { return "", err } return fullName, nil default: - return "", fmt.Errorf("unknown field type %#v", field) + return getGenericFieldType(file, field, nil) } } @@ -1428,7 +1459,7 @@ func defineTypeOfExample(schemaType, arrayType, exampleValue string) (interface{ result := map[string]interface{}{} for _, value := range values { - mapData := strings.Split(value, ":") + mapData := strings.SplitN(value, ":", 2) if len(mapData) == 2 { v, err := defineTypeOfExample(arrayType, "", mapData[1]) @@ -1439,7 +1470,6 @@ func defineTypeOfExample(schemaType, arrayType, exampleValue string) (interface{ result[mapData[0]] = v continue - } return nil, fmt.Errorf("example value %s should format: key:value", exampleValue) @@ -1485,7 +1515,7 @@ func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg) error { srcDir := pkg.Raw.Dir - files, err := ioutil.ReadDir(srcDir) // only parsing files in the dir(don't contain sub dir files) + files, err := os.ReadDir(srcDir) // only parsing files in the dir(don't contain sub dir files) if err != nil { return err } @@ -1515,18 +1545,7 @@ func (parser *Parser) parseFile(packageDir, path string, src interface{}) error return nil } - // 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) - } - - err = parser.packages.CollectAstFile(packageDir, path, astFile) - if err != nil { - return err - } - - return nil + return parser.packages.ParseFile(packageDir, path, src) } func (parser *Parser) checkOperationIDUniqueness() error { @@ -1574,7 +1593,7 @@ func walkWith(excludes map[string]struct{}, parseVendor bool) func(path string, if f.IsDir() { if !parseVendor && f.Name() == "vendor" || // ignore "vendor" f.Name() == "docs" || // exclude docs - len(f.Name()) > 1 && f.Name()[0] == '.' { // exclude all hidden folder + len(f.Name()) > 1 && f.Name()[0] == '.' && f.Name() != ".." { // exclude all hidden folder return filepath.SkipDir } diff --git a/parser_test.go b/parser_test.go index 07ac66518..c7e5bacb1 100644 --- a/parser_test.go +++ b/parser_test.go @@ -7,7 +7,6 @@ import ( "go/ast" goparser "go/parser" "go/token" - "io/ioutil" "log" "os" "path/filepath" @@ -78,6 +77,34 @@ func TestSetOverrides(t *testing.T) { assert.Equal(t, overrides, p.Overrides) } +func TestOverrides_getTypeSchema(t *testing.T) { + t.Parallel() + + overrides := map[string]string{ + "sql.NullString": "string", + } + + p := New(SetOverrides(overrides)) + + t.Run("Override sql.NullString by string", func(t *testing.T) { + t.Parallel() + + s, err := p.getTypeSchema("sql.NullString", nil, false) + if assert.NoError(t, err) { + assert.Truef(t, s.Type.Contains("string"), "type sql.NullString should be overridden by string") + } + }) + + t.Run("Missing Override for sql.NullInt64", func(t *testing.T) { + t.Parallel() + + _, err := p.getTypeSchema("sql.NullInt64", nil, false) + if assert.Error(t, err) { + assert.Equal(t, "cannot find type definition: sql.NullInt64", err.Error()) + } + }) +} + func TestParser_ParseDefinition(t *testing.T) { p := New() @@ -120,6 +147,28 @@ func TestParser_ParseDefinition(t *testing.T) { } _, err = p.ParseDefinition(definition) assert.Error(t, err) + + // Parsing *ast.FuncType with parent spec + definition = &TypeSpecDef{ + PkgPath: "github.com/swagger/swag/model", + File: &ast.File{ + Name: &ast.Ident{ + Name: "model", + }, + }, + TypeSpec: &ast.TypeSpec{ + Name: &ast.Ident{ + Name: "Test", + }, + Type: &ast.FuncType{}, + }, + ParentSpec: &ast.FuncDecl{ + Name: ast.NewIdent("TestFuncDecl"), + }, + } + _, err = p.ParseDefinition(definition) + assert.Error(t, err) + assert.Equal(t, "model.TestFuncDecl.Test", definition.TypeName()) } func TestParser_ParseGeneralApiInfo(t *testing.T) { @@ -479,7 +528,7 @@ func TestParser_ParseProduceComment(t *testing.T) { assert.Equal(t, parser.swagger.Produces, expected) } -func TestParser_ParseGeneralAPIInfoCollectionFromat(t *testing.T) { +func TestParser_ParseGeneralAPIInfoCollectionFormat(t *testing.T) { t.Parallel() parser := New() @@ -765,16 +814,13 @@ 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) + _ = p.packages.ParseFile("api", "api/api.go", src) - _, err = p.packages.ParseTypes() + _, err := p.packages.ParseTypes() assert.NoError(t, err) - err = p.ParseRouterAPIInfo("", f) + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") @@ -814,7 +860,7 @@ func TestParser_ParseType(t *testing.T) { func TestParseSimpleApi1(t *testing.T) { t.Parallel() - expected, err := ioutil.ReadFile("testdata/simple/expected.json") + expected, err := os.ReadFile("testdata/simple/expected.json") assert.NoError(t, err) searchDir := "testdata/simple" p := New() @@ -826,6 +872,20 @@ func TestParseSimpleApi1(t *testing.T) { assert.JSONEq(t, string(expected), string(b)) } +func TestParseInterfaceAndError(t *testing.T) { + t.Parallel() + + expected, err := os.ReadFile("testdata/error/expected.json") + assert.NoError(t, err) + searchDir := "testdata/error" + p := New() + err = p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.NoError(t, err) + + b, _ := json.MarshalIndent(p.swagger, "", " ") + assert.JSONEq(t, string(expected), string(b)) +} + func TestParseSimpleApi_ForSnakecase(t *testing.T) { t.Parallel() @@ -2044,7 +2104,7 @@ func TestParseComposition(t *testing.T) { err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) - expected, err := ioutil.ReadFile(filepath.Join(searchDir, "expected.json")) + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") @@ -2061,7 +2121,7 @@ func TestParseImportAliases(t *testing.T) { err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) - expected, err := ioutil.ReadFile(filepath.Join(searchDir, "expected.json")) + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") @@ -2081,7 +2141,7 @@ func TestParseTypeOverrides(t *testing.T) { err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) - expected, err := ioutil.ReadFile(filepath.Join(searchDir, "expected.json")) + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") @@ -2093,12 +2153,11 @@ func TestParseNested(t *testing.T) { t.Parallel() searchDir := "testdata/nested" - p := New() - p.ParseDependency = true + p := New(SetParseDependency(true)) err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) - expected, err := ioutil.ReadFile(filepath.Join(searchDir, "expected.json")) + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") @@ -2109,8 +2168,7 @@ func TestParseDuplicated(t *testing.T) { t.Parallel() searchDir := "testdata/duplicated" - p := New() - p.ParseDependency = true + p := New(SetParseDependency(true)) err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.Errorf(t, err, "duplicated @id declarations successfully found") } @@ -2119,8 +2177,16 @@ func TestParseDuplicatedOtherMethods(t *testing.T) { t.Parallel() searchDir := "testdata/duplicated2" - p := New() - p.ParseDependency = true + p := New(SetParseDependency(true)) + err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) + assert.Errorf(t, err, "duplicated @id declarations successfully found") +} + +func TestParseDuplicatedFunctionScoped(t *testing.T) { + t.Parallel() + + searchDir := "testdata/duplicated_function_scoped" + p := New(SetParseDependency(true)) err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.Errorf(t, err, "duplicated @id declarations successfully found") } @@ -2129,12 +2195,11 @@ func TestParseConflictSchemaName(t *testing.T) { t.Parallel() searchDir := "testdata/conflict_name" - p := New() - p.ParseDependency = true + p := New(SetParseDependency(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")) + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } @@ -2142,22 +2207,19 @@ func TestParseConflictSchemaName(t *testing.T) { func TestParseExternalModels(t *testing.T) { searchDir := "testdata/external_models/main" mainAPIFile := "main.go" - p := New() - p.ParseDependency = true + p := New(SetParseDependency(true)) err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") //ioutil.WriteFile("./testdata/external_models/main/expected.json",b,0777) - expected, err := ioutil.ReadFile(filepath.Join(searchDir, "expected.json")) + expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json")) assert.NoError(t, err) assert.Equal(t, string(expected), string(b)) } func TestParseGoList(t *testing.T) { mainAPIFile := "main.go" - p := New(ParseUsingGoList(true)) - p.ParseDependency = true - + p := New(ParseUsingGoList(true), SetParseDependency(true)) go111moduleEnv := os.Getenv("GO111MODULE") cases := []struct { @@ -2309,15 +2371,13 @@ func Test(){ } } }` - 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() + _ = p.packages.ParseFile("api", "api/api.go", src) + _, err := p.packages.ParseTypes() assert.NoError(t, err) - err = p.ParseRouterAPIInfo("", f) + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) assert.NoError(t, err) out, err := json.MarshalIndent(p.swagger.Definitions, "", " ") @@ -2371,21 +2431,16 @@ type ResponseWrapper struct { } } }` - parser := New() - parser.ParseDependency = true + parser := New(SetParseDependency(true)) - f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments) - assert.NoError(t, err) - _ = parser.packages.CollectAstFile("api", "api/api.go", f) + _ = parser.packages.ParseFile("api", "api/api.go", src) - f2, err := goparser.ParseFile(token.NewFileSet(), "", restsrc, goparser.ParseComments) - assert.NoError(t, err) - _ = parser.packages.CollectAstFile("rest", "rest/rest.go", f2) + _ = parser.packages.ParseFile("rest", "rest/rest.go", restsrc) - _, err = parser.packages.ParseTypes() + _, err := parser.packages.ParseTypes() assert.NoError(t, err) - err = parser.ParseRouterAPIInfo("", f) + err = parser.packages.RangeFiles(parser.ParseRouterAPIInfo) assert.NoError(t, err) out, err := json.MarshalIndent(parser.swagger.Definitions, "", " ") @@ -2433,21 +2488,21 @@ func Test(){ }, "test2": { "description": "test2", - "$ref": "#/definitions/api.Child" + "allOf": [ + { + "$ref": "#/definitions/api.Child" + } + ] } } } }` - - 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() + _ = p.packages.ParseFile("api", "api/api.go", src) + _, err := p.packages.ParseTypes() assert.NoError(t, err) - err = p.ParseRouterAPIInfo("", f) + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) assert.NoError(t, err) out, err := json.MarshalIndent(p.swagger.Definitions, "", " ") @@ -2537,7 +2592,11 @@ func Test(){ }, "test6": { "description": "test6", - "$ref": "#/definitions/api.MyMapType" + "allOf": [ + { + "$ref": "#/definitions/api.MyMapType" + } + ] }, "test7": { "description": "test7", @@ -2566,16 +2625,13 @@ func Test(){ } } }` - f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments) - assert.NoError(t, err) - p := New() - _ = p.packages.CollectAstFile("api", "api/api.go", f) + _ = p.packages.ParseFile("api", "api/api.go", src) - _, err = p.packages.ParseTypes() + _, err := p.packages.ParseTypes() assert.NoError(t, err) - err = p.ParseRouterAPIInfo("", f) + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) assert.NoError(t, err) out, err := json.MarshalIndent(p.swagger.Definitions, "", " ") @@ -3007,8 +3063,7 @@ func TestParseOutsideDependencies(t *testing.T) { searchDir := "testdata/pare_outside_dependencies" mainAPIFile := "cmd/main.go" - p := New() - p.ParseDependency = true + p := New(SetParseDependency(true)) if err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth); err != nil { t.Error("Failed to parse api: " + err.Error()) } @@ -3063,7 +3118,7 @@ func Fun() { ], "responses": { "200": { - "description": "" + "description": "OK" } } } @@ -3071,16 +3126,14 @@ 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.ParseFile("api", "api/api.go", src) + assert.NoError(t, err) _, err = p.packages.ParseTypes() assert.NoError(t, err) - err = p.ParseRouterAPIInfo("", f) + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") @@ -3120,7 +3173,7 @@ func Fun() { ], "responses": { "200": { - "description": "" + "description": "OK" } } } @@ -3128,16 +3181,13 @@ 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) + _ = p.packages.ParseFile("api", "api/api.go", src) - _, err = p.packages.ParseTypes() + _, err := p.packages.ParseTypes() assert.NoError(t, err) - err = p.ParseRouterAPIInfo("", f) + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) assert.NoError(t, err) b, _ := json.MarshalIndent(p.swagger, "", " ") @@ -3166,15 +3216,13 @@ 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() + _ = p.packages.ParseFile("api", "api/api.go", src) + _, err := p.packages.ParseTypes() assert.NoError(t, err) - err = p.ParseRouterAPIInfo("", f) + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) assert.NoError(t, err) assert.NoError(t, err) @@ -3191,27 +3239,141 @@ func Fun() { assert.Equal(t, "#/definitions/Teacher", ref.String()) } -func TestPackagesDefinitions_CollectAstFileInit(t *testing.T) { +func TestParseFunctionScopedStructDefinition(t *testing.T) { t.Parallel() src := ` package main -// @Router /test [get] +// @Param request body main.Fun.request true "query params" +// @Success 200 {object} main.Fun.response +// @Router /test [post] func Fun() { + type request struct { + Name string + } + + type response struct { + Name string + Child string + } +} +` + p := New() + _ = p.packages.ParseFile("api", "api/api.go", src) + _, err := p.packages.ParseTypes() + assert.NoError(t, err) + + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) + assert.NoError(t, err) + + _, ok := p.swagger.Definitions["main.Fun.response"] + assert.True(t, ok) +} + +func TestParseFunctionScopedStructRequestResponseJSON(t *testing.T) { + t.Parallel() + src := ` +package main + +// @Param request body main.Fun.request true "query params" +// @Success 200 {object} main.Fun.response +// @Router /test [post] +func Fun() { + type request struct { + Name string + } + + type response struct { + Name string + Child string + } } ` - f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments) + expected := `{ + "info": { + "contact": {} + }, + "paths": { + "/test": { + "post": { + "parameters": [ + { + "description": "query params", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Fun.request" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Fun.response" + } + } + } + } + } + }, + "definitions": { + "main.Fun.request": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "main.Fun.response": { + "type": "object", + "properties": { + "child": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } +}` + + p := New() + _ = p.packages.ParseFile("api", "api/api.go", src) + + _, err := p.packages.ParseTypes() + assert.NoError(t, err) + + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) assert.NoError(t, err) + b, _ := json.MarshalIndent(p.swagger, "", " ") + assert.Equal(t, expected, string(b)) +} + +func TestPackagesDefinitions_CollectAstFileInit(t *testing.T) { + t.Parallel() + + src := ` +package main + +// @Router /test [get] +func Fun() { + +} +` pkgs := NewPackagesDefinitions() - // unset the .files and .packages and check that they're re-initialized by CollectAstFile + // unset the .files and .packages and check that they're re-initialized by collectAstFile pkgs.packages = nil pkgs.files = nil - _ = pkgs.CollectAstFile("api", "api/api.go", f) + _ = pkgs.ParseFile("api", "api/api.go", src) assert.NotNil(t, pkgs.packages) assert.NotNil(t, pkgs.files) } @@ -3227,18 +3389,23 @@ 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) - assert.NotNil(t, p.packages.files[f]) - - astFileInfo := p.packages.files[f] + _ = p.packages.ParseFile("api", "api/api.go", src) + assert.Equal(t, 1, len(p.packages.files)) + var path string + var file *ast.File + for path, file = range p.packages.packages["api"].Files { + break + } + assert.NotNil(t, file) + assert.NotNil(t, p.packages.files[file]) // if we collect the same again nothing should happen - _ = p.packages.CollectAstFile("api", "api/api.go", f) - assert.Equal(t, astFileInfo, p.packages.files[f]) + _ = p.packages.ParseFile("api", "api/api.go", src) + assert.Equal(t, 1, len(p.packages.files)) + assert.Equal(t, file, p.packages.packages["api"].Files[path]) + assert.NotNil(t, p.packages.files[file]) } func TestParseJSONFieldString(t *testing.T) { @@ -3284,7 +3451,7 @@ func TestParseJSONFieldString(t *testing.T) { } }, "500": { - "description": "" + "description": "Internal Server Error" } } } @@ -3366,13 +3533,11 @@ 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.ParseFile("api", "api/api.go", src) + assert.NoError(t, err) _, _ = p.packages.ParseTypes() - err = p.ParseRouterAPIInfo("", f) + err = p.packages.RangeFiles(p.ParseRouterAPIInfo) assert.NoError(t, err) teacher, ok := p.swagger.Definitions["Teacher"] @@ -3515,6 +3680,7 @@ func TestParser_Skip(t *testing.T) { assert.NoError(t, parser.Skip("", &mockFS{FileName: "models", IsDirectory: true})) assert.NoError(t, parser.Skip("", &mockFS{FileName: "admin", IsDirectory: true})) assert.NoError(t, parser.Skip("", &mockFS{FileName: "release", IsDirectory: true})) + assert.NoError(t, parser.Skip("", &mockFS{FileName: "..", IsDirectory: true})) parser = New(SetExcludedDirsAndFiles("admin/release,admin/models")) assert.NoError(t, parser.Skip("admin", &mockFS{IsDirectory: true})) @@ -3526,28 +3692,28 @@ func TestParser_Skip(t *testing.T) { func TestGetFieldType(t *testing.T) { t.Parallel() - field, err := getFieldType(&ast.Ident{Name: "User"}) + field, err := getFieldType(&ast.File{}, &ast.Ident{Name: "User"}) assert.NoError(t, err) assert.Equal(t, "User", field) - _, err = getFieldType(&ast.FuncType{}) + _, err = getFieldType(&ast.File{}, &ast.FuncType{}) assert.Error(t, err) - field, err = getFieldType(&ast.SelectorExpr{X: &ast.Ident{Name: "models"}, Sel: &ast.Ident{Name: "User"}}) + field, err = getFieldType(&ast.File{}, &ast.SelectorExpr{X: &ast.Ident{Name: "models"}, Sel: &ast.Ident{Name: "User"}}) assert.NoError(t, err) assert.Equal(t, "models.User", field) - _, err = getFieldType(&ast.SelectorExpr{X: &ast.FuncType{}, Sel: &ast.Ident{Name: "User"}}) + _, err = getFieldType(&ast.File{}, &ast.SelectorExpr{X: &ast.FuncType{}, Sel: &ast.Ident{Name: "User"}}) assert.Error(t, err) - field, err = getFieldType(&ast.StarExpr{X: &ast.Ident{Name: "User"}}) + field, err = getFieldType(&ast.File{}, &ast.StarExpr{X: &ast.Ident{Name: "User"}}) assert.NoError(t, err) assert.Equal(t, "User", field) - field, err = getFieldType(&ast.StarExpr{X: &ast.FuncType{}}) + field, err = getFieldType(&ast.File{}, &ast.StarExpr{X: &ast.FuncType{}}) assert.Error(t, err) - field, err = getFieldType(&ast.StarExpr{X: &ast.SelectorExpr{X: &ast.Ident{Name: "models"}, Sel: &ast.Ident{Name: "User"}}}) + field, err = getFieldType(&ast.File{}, &ast.StarExpr{X: &ast.SelectorExpr{X: &ast.Ident{Name: "models"}, Sel: &ast.Ident{Name: "User"}}}) assert.NoError(t, err) assert.Equal(t, "models.User", field) } @@ -3633,3 +3799,82 @@ func TestTryAddDescription(t *testing.T) { }) } } + +func Test_getTagsFromComment(t *testing.T) { + type args struct { + comment string + } + tests := []struct { + name string + args args + wantTags []string + }{ + { + name: "no tags comment", + args: args{ + comment: "//@name Student", + }, + wantTags: nil, + }, + { + name: "empty comment", + args: args{ + comment: "//", + }, + wantTags: nil, + }, + { + name: "tags comment", + args: args{ + comment: "//@Tags tag1,tag2,tag3", + }, + wantTags: []string{"tag1", "tag2", "tag3"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotTags := getTagsFromComment(tt.args.comment); !reflect.DeepEqual(gotTags, tt.wantTags) { + t.Errorf("getTagsFromComment() = %v, want %v", gotTags, tt.wantTags) + } + }) + } +} + +func TestParser_matchTags(t *testing.T) { + + type args struct { + comments []*ast.Comment + } + tests := []struct { + name string + parser *Parser + args args + wantMatch bool + }{ + { + name: "no tags filter", + parser: New(), + args: args{comments: []*ast.Comment{{Text: "//@Tags tag1,tag2,tag3"}}}, + wantMatch: true, + }, + { + name: "with tags filter but no match", + parser: New(SetTags("tag4,tag5,!tag1")), + args: args{comments: []*ast.Comment{{Text: "//@Tags tag1,tag2,tag3"}}}, + wantMatch: false, + }, + { + name: "with tags filter but match", + parser: New(SetTags("tag4,tag5,tag1")), + args: args{comments: []*ast.Comment{{Text: "//@Tags tag1,tag2,tag3"}}}, + wantMatch: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotMatch := tt.parser.matchTags(tt.args.comments); gotMatch != tt.wantMatch { + t.Errorf("Parser.matchTags() = %v, want %v", gotMatch, tt.wantMatch) + } + }) + } +} diff --git a/schema.go b/schema.go index a23d21b36..ba92ac663 100644 --- a/schema.go +++ b/schema.go @@ -3,8 +3,6 @@ package swag import ( "errors" "fmt" - "go/ast" - "strings" "github.com/go-openapi/spec" ) @@ -26,12 +24,17 @@ const ( STRING = "string" // FUNC represent a function value. FUNC = "func" + // ERROR represent a error value. + ERROR = "error" // INTERFACE represent a interface value. INTERFACE = "interface{}" // ANY represent a any value. ANY = "any" // NIL represent a empty value. NIL = "nil" + + // IgnoreNameOverridePrefix Prepend to model to avoid renaming based on comment. + IgnoreNameOverridePrefix = '$' ) // CheckSchemaType checks if typeName is not a name of primitive type. @@ -63,6 +66,11 @@ func IsPrimitiveType(typeName string) bool { return false } +// IsInterfaceLike determines whether the swagger type name is an go named interface type like error type. +func IsInterfaceLike(typeName string) bool { + return typeName == ERROR || typeName == ANY +} + // IsNumericType determines whether the swagger type name is a numeric type. func IsNumericType(typeName string) bool { return typeName == INTEGER || typeName == NUMBER @@ -106,8 +114,7 @@ func IsGolangPrimitiveType(typeName string) bool { "float32", "float64", "bool", - "string", - "any": + "string": return true } @@ -124,24 +131,34 @@ func TransToValidCollectionFormat(format string) string { 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 { - texts := strings.Split(strings.TrimSpace(strings.TrimLeft(comment.Text, "/")), " ") - if len(texts) > 1 && strings.ToLower(texts[0]) == "@name" { - return texts[1] - } - } - } +func ignoreNameOverride(name string) bool { + return len(name) != 0 && name[0] == IgnoreNameOverridePrefix +} - if spec.Name != nil { - return fullTypeName(strings.Split(pkgName, ".")[0], spec.Name.Name) +// IsComplexSchema whether a schema is complex and should be a ref schema +func IsComplexSchema(schema *spec.Schema) bool { + // a enum type should be complex + if len(schema.Enum) > 0 { + return true + } + + // a deep array type is complex, how to determine deep? here more than 2 ,for example: [][]object,[][][]int + if len(schema.Type) > 2 { + return true + } + + //Object included, such as Object or []Object + for _, st := range schema.Type { + if st == OBJECT { + return true } } + return false +} - return pkgName +// IsRefSchema whether a schema is a reference schema. +func IsRefSchema(schema *spec.Schema) bool { + return schema.Ref.Ref.GetURL() != nil } // RefSchema build a reference schema. diff --git a/schema_test.go b/schema_test.go index 0fe60e816..6589e2e54 100644 --- a/schema_test.go +++ b/schema_test.go @@ -1,7 +1,6 @@ package swag import ( - "go/ast" "testing" "github.com/go-openapi/spec" @@ -142,19 +141,11 @@ func TestIsNumericType(t *testing.T) { assert.Equal(t, IsNumericType(STRING), false) } -func TestTypeDocName(t *testing.T) { +func TestIsInterfaceLike(t *testing.T) { t.Parallel() - expected := "a/package" - assert.Equal(t, expected, TypeDocName(expected, nil)) + assert.Equal(t, IsInterfaceLike(ERROR), true) + assert.Equal(t, IsInterfaceLike(ANY), true) - expected = "package.Model" - assert.Equal(t, expected, TypeDocName("package", &ast.TypeSpec{Name: &ast.Ident{Name: "Model"}})) - - expected = "Model" - assert.Equal(t, expected, TypeDocName("package", &ast.TypeSpec{ - Comment: &ast.CommentGroup{ - List: []*ast.Comment{{Text: "// @name Model"}}, - }, - })) + assert.Equal(t, IsInterfaceLike(STRING), false) } diff --git a/swagger.go b/swagger.go index 5ffbab63e..74c162c28 100644 --- a/swagger.go +++ b/swagger.go @@ -39,6 +39,15 @@ func Register(name string, swagger Swagger) { swags[name] = swagger } +// GetSwagger returns the swagger instance for given name. +// If not found, returns nil. +func GetSwagger(name string) Swagger { + swaggerMu.RLock() + defer swaggerMu.RUnlock() + + return swags[name] +} + // ReadDoc reads swagger document. An optional name parameter can be passed to read a specific document. // The default name is "swagger". func ReadDoc(optionalName ...string) (string, error) { diff --git a/swagger_test.go b/swagger_test.go index 3d15d0b59..043190508 100644 --- a/swagger_test.go +++ b/swagger_test.go @@ -219,3 +219,14 @@ func TestCalledTwicelRegister(t *testing.T) { func setup() { swags = nil } + +func TestGetSwagger(t *testing.T) { + setup() + instance := &s{} + Register(Name, instance) + swagger := GetSwagger(Name) + assert.Equal(t, instance, swagger) + + swagger = GetSwagger("invalid") + assert.Nil(t, swagger) +} diff --git a/testdata/conflict_name/expected.json b/testdata/conflict_name/expected.json index 74f046c46..0b3576dbc 100644 --- a/testdata/conflict_name/expected.json +++ b/testdata/conflict_name/expected.json @@ -24,7 +24,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github.com_swaggo_swag_testdata_conflict_name_model.ErrorsResponse" + "$ref": "#/definitions/github_com_swaggo_swag_testdata_conflict_name_model.ErrorsResponse" } } } @@ -47,7 +47,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github.com_swaggo_swag_testdata_conflict_name_model2.ErrorsResponse" + "$ref": "#/definitions/github_com_swaggo_swag_testdata_conflict_name_model2.ErrorsResponse" } } } @@ -55,7 +55,7 @@ } }, "definitions": { - "github.com_swaggo_swag_testdata_conflict_name_model.ErrorsResponse": { + "github_com_swaggo_swag_testdata_conflict_name_model.ErrorsResponse": { "type": "object", "properties": { "newTime": { @@ -63,7 +63,7 @@ } } }, - "github.com_swaggo_swag_testdata_conflict_name_model.MyStruct": { + "github_com_swaggo_swag_testdata_conflict_name_model.MyStruct": { "type": "object", "properties": { "name": { @@ -71,7 +71,7 @@ } } }, - "github.com_swaggo_swag_testdata_conflict_name_model2.ErrorsResponse": { + "github_com_swaggo_swag_testdata_conflict_name_model2.ErrorsResponse": { "type": "object", "properties": { "newTime": { @@ -79,7 +79,7 @@ } } }, - "github.com_swaggo_swag_testdata_conflict_name_model2.MyStruct": { + "github_com_swaggo_swag_testdata_conflict_name_model2.MyStruct": { "type": "object", "properties": { "name": { @@ -91,7 +91,7 @@ "type": "object", "properties": { "my": { - "$ref": "#/definitions/github.com_swaggo_swag_testdata_conflict_name_model.MyStruct" + "$ref": "#/definitions/github_com_swaggo_swag_testdata_conflict_name_model.MyStruct" }, "name": { "type": "string" @@ -102,7 +102,7 @@ "type": "object", "properties": { "my": { - "$ref": "#/definitions/github.com_swaggo_swag_testdata_conflict_name_model2.MyStruct" + "$ref": "#/definitions/github_com_swaggo_swag_testdata_conflict_name_model2.MyStruct" }, "name": { "type": "string" diff --git a/testdata/duplicated_function_scoped/api/api.go b/testdata/duplicated_function_scoped/api/api.go new file mode 100644 index 000000000..25bfcc809 --- /dev/null +++ b/testdata/duplicated_function_scoped/api/api.go @@ -0,0 +1,12 @@ +package api + +import "net/http" + +// @Description get Foo +// @ID get-foo +// @Success 200 {object} api.GetFoo.response +// @Router /testapi/get-foo [get] +func GetFoo(w http.ResponseWriter, r *http.Request) { + type response struct { + } +} diff --git a/testdata/duplicated_function_scoped/main.go b/testdata/duplicated_function_scoped/main.go new file mode 100644 index 000000000..d5f34680f --- /dev/null +++ b/testdata/duplicated_function_scoped/main.go @@ -0,0 +1,22 @@ +package composition + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/duplicated_function_scoped/api" + otherapi "github.com/swaggo/swag/testdata/duplicated_function_scoped/other_api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server +// @termsOfService http://swagger.io/terms/ + +// @host petstore.swagger.io +// @BasePath /v2 + +func main() { + http.HandleFunc("/testapi/get-foo", api.GetFoo) + http.HandleFunc("/testapi/post-bar", otherapi.GetFoo) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/duplicated_function_scoped/other_api/api.go b/testdata/duplicated_function_scoped/other_api/api.go new file mode 100644 index 000000000..25bfcc809 --- /dev/null +++ b/testdata/duplicated_function_scoped/other_api/api.go @@ -0,0 +1,12 @@ +package api + +import "net/http" + +// @Description get Foo +// @ID get-foo +// @Success 200 {object} api.GetFoo.response +// @Router /testapi/get-foo [get] +func GetFoo(w http.ResponseWriter, r *http.Request) { + type response struct { + } +} diff --git a/testdata/enums/api/api.go b/testdata/enums/api/api.go new file mode 100644 index 000000000..ea1766be0 --- /dev/null +++ b/testdata/enums/api/api.go @@ -0,0 +1,13 @@ +package api + +import "github.com/swaggo/swag/testdata/enums/types" + +// enum example +// +// @Summary enums +// @Description enums +// @Failure 400 {object} types.Person "ok" +// @Router /students [post] +func API() { + _ = types.Person{} +} diff --git a/testdata/enums/consts/const.go b/testdata/enums/consts/const.go new file mode 100644 index 000000000..83dc97fa7 --- /dev/null +++ b/testdata/enums/consts/const.go @@ -0,0 +1,12 @@ +package consts + +const Base = 1 + +const uintSize = 32 << (^uint(uintptr(0)) >> 63) +const maxBase = 10 + ('z' - 'a' + 1) + ('Z' - 'A' + 1) +const shlByLen = 1 << len("aaa") +const hexnum = 0xFF +const octnum = 017 +const nonescapestr = `aa\nbb\u8888cc` +const escapestr = "aa\nbb\u8888cc" +const escapechar = '\u8888' diff --git a/testdata/enums/expected.json b/testdata/enums/expected.json new file mode 100644 index 000000000..916f116ea --- /dev/null +++ b/testdata/enums/expected.json @@ -0,0 +1,116 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample 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" + }, + "basePath": "/v2", + "paths": { + "/students": { + "post": { + "description": "enums", + "summary": "enums", + "responses": { + "400": { + "description": "ok", + "schema": { + "$ref": "#/definitions/types.Person" + } + } + } + } + } + }, + "definitions": { + "types.Class": { + "type": "integer", + "enum": [ + -1, + 1, + 2, + 3, + 4, + 5 + ], + "x-enum-comments": { + "A": "AAA", + "B": "BBB" + }, + "x-enum-varnames": [ + "None", + "A", + "B", + "C", + "D", + "F" + ] + }, + "types.Mask": { + "type": "integer", + "enum": [ + 1, + 2, + 4, + 8 + ], + "x-enum-comments": { + "Mask1": "Mask1", + "Mask2": "Mask2", + "Mask3": "Mask3", + "Mask4": "Mask4" + }, + "x-enum-varnames": [ + "Mask1", + "Mask2", + "Mask3", + "Mask4" + ] + }, + "types.Person": { + "type": "object", + "properties": { + "class": { + "$ref": "#/definitions/types.Class" + }, + "mask": { + "$ref": "#/definitions/types.Mask" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/types.Type" + } + } + }, + "types.Type": { + "type": "string", + "enum": [ + "teacher", + "student", + "Other" + ], + "x-enum-comments": { + "Other": "Other", + "Student": "student", + "Teacher": "teacher" + }, + "x-enum-varnames": [ + "Teacher", + "Student", + "Other" + ] + } + } +} \ No newline at end of file diff --git a/testdata/enums/main.go b/testdata/enums/main.go new file mode 100644 index 000000000..b3c29b55d --- /dev/null +++ b/testdata/enums/main.go @@ -0,0 +1,17 @@ +package main + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample 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 + +// @BasePath /v2 +func main() { +} diff --git a/testdata/enums/types/model.go b/testdata/enums/types/model.go new file mode 100644 index 000000000..8438a8017 --- /dev/null +++ b/testdata/enums/types/model.go @@ -0,0 +1,57 @@ +package types + +import ( + "github.com/swaggo/swag/testdata/enums/consts" +) + +type Class int + +const ( + None Class = -1 + A Class = consts.Base + (iota+1-1)*2/2%100 - (1&1 | 1) + (2 ^ 2) // AAA + B /* BBB */ + C + D = C + 1 + F = Class(5) + //G is not enum + G = H + 10 + //H is not enum + H = 10 + //I is not enum + I = int(F + 2) +) + +const J = 1 << uint16(I) + +type Mask int + +const ( + Mask1 Mask = 0x02 << iota >> 1 // Mask1 + Mask2 /* Mask2 */ + Mask3 // Mask3 + Mask4 // Mask4 +) + +type Type string + +const ( + Teacher Type = "teacher" // teacher + Student Type = "student" /* student */ + Other Type = "Other" // Other + Unknown = "Unknown" + OtherUnknown = string(Other + Unknown) +) + +type Sex rune + +const ( + Male Sex = 'M' + Female = 'F' +) + +type Person struct { + Name string + Class Class + Mask Mask + Type Type +} diff --git a/testdata/error/api/api.go b/testdata/error/api/api.go new file mode 100644 index 000000000..8c59da5c7 --- /dev/null +++ b/testdata/error/api/api.go @@ -0,0 +1,23 @@ +package api + +import ( + "net/http" + + . "github.com/swaggo/swag/testdata/error/errors" + _ "github.com/swaggo/swag/testdata/error/web" +) + +// Upload do something +// @Summary Upload file +// @Description Upload file +// @ID file.upload +// @Accept multipart/form-data +// @Produce json +// @Param file formData file true "this is a test file" +// @Success 200 {string} string "ok" +// @Failure 400 {object} web.CrossErrors "Abort !!" +// @Router /file/upload [post] +func Upload(w http.ResponseWriter, r *http.Request) { + //write your code + _ = Errors{} +} diff --git a/testdata/error/errors/errors.go b/testdata/error/errors/errors.go new file mode 100644 index 000000000..60349c12e --- /dev/null +++ b/testdata/error/errors/errors.go @@ -0,0 +1,14 @@ +package errors + +// CustomInterface some interface +type CustomInterface interface { + Error() string +} + +// Errors errors and interfaces +type Errors struct { + Error error + ErrorInterface CustomInterface + Interface interface{} + Any any +} diff --git a/testdata/error/expected.json b/testdata/error/expected.json new file mode 100644 index 000000000..a188df22f --- /dev/null +++ b/testdata/error/expected.json @@ -0,0 +1,69 @@ +{ + "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": [ + "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": "Abort !!", + "schema": { + "$ref": "#/definitions/web.CrossErrors" + } + } + } + } + } + }, + "definitions": { + "web.CrossErrors": { + "type": "object", + "properties": { + "any": {}, + "error": {}, + "errorInterface": {}, + "interface": {} + } + } + } +} \ No newline at end of file diff --git a/testdata/error/main.go b/testdata/error/main.go new file mode 100644 index 000000000..249c0bb85 --- /dev/null +++ b/testdata/error/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/error/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host petstore.swagger.io +// @BasePath /v2 + +func main() { + http.HandleFunc("/testapi/upload", api.Upload) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/error/web/handler.go b/testdata/error/web/handler.go new file mode 100644 index 000000000..c46f90a9d --- /dev/null +++ b/testdata/error/web/handler.go @@ -0,0 +1,7 @@ +package web + +import ( + "github.com/swaggo/swag/testdata/error/errors" +) + +type CrossErrors errors.Errors diff --git a/testdata/generics_arrays/api/api.go b/testdata/generics_arrays/api/api.go new file mode 100644 index 000000000..66ef1bb02 --- /dev/null +++ b/testdata/generics_arrays/api/api.go @@ -0,0 +1,46 @@ +package api + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/generics_arrays/types" + "github.com/swaggo/swag/testdata/generics_arrays/web" +) + +// @Summary List Posts +// @Description Get All of the Posts +// @Accept json +// @Produce json +// @Param data body web.GenericListBody[types.Post] true "Some ID" +// @Success 200 {object} web.GenericListResponse[types.Post] +// @Success 222 {object} web.GenericListResponseMulti[types.Post, types.Post] +// @Router /posts [get] +func GetPosts(w http.ResponseWriter, r *http.Request) { + _ = web.GenericListResponseMulti[types.Post, types.Post]{} +} + +// @Summary Add new pets to the store +// @Description get string by ID +// @Accept json +// @Produce json +// @Param data body web.GenericListBodyMulti[types.Post, types.Post] true "Some ID" +// @Success 200 {object} web.GenericListResponse[types.Post] +// @Success 222 {object} web.GenericListResponseMulti[types.Post, types.Post] +// @Router /posts-multi [get] +func GetPostMulti(w http.ResponseWriter, r *http.Request) { + //write your code + _ = web.GenericListResponseMulti[types.Post, types.Post]{} +} + +// @Summary Add new pets to the store +// @Description get string by ID +// @Accept json +// @Produce json +// @Param data body web.GenericListBodyMulti[types.Post, []types.Post] true "Some ID" +// @Success 200 {object} web.GenericListResponse[[]types.Post] +// @Success 222 {object} web.GenericListResponseMulti[types.Post, []types.Post] +// @Router /posts-multis [get] +func GetPostArray(w http.ResponseWriter, r *http.Request) { + //write your code + _ = web.GenericListResponseMulti[types.Post, []types.Post]{} +} diff --git a/testdata/generics_arrays/expected.json b/testdata/generics_arrays/expected.json new file mode 100644 index 000000000..782ffb979 --- /dev/null +++ b/testdata/generics_arrays/expected.json @@ -0,0 +1,289 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server.", + "title": "Swagger Example API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:4000", + "basePath": "/api", + "paths": { + "/posts": { + "get": { + "description": "Get All of the Posts", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "List Posts", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/web.GenericListBody-types_Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.GenericListResponse-types_Post" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/web.GenericListResponseMulti-types_Post-types_Post" + } + } + } + } + }, + "/posts-multi": { + "get": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add new pets to the store", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/web.GenericListBodyMulti-types_Post-types_Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.GenericListResponse-types_Post" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/web.GenericListResponseMulti-types_Post-types_Post" + } + } + } + } + }, + "/posts-multis": { + "get": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add new pets to the store", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/web.GenericListBodyMulti-types_Post-array_types_Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.GenericListResponse-array_types_Post" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/web.GenericListResponseMulti-types_Post-array_types_Post" + } + } + } + } + } + }, + "definitions": { + "types.Post": { + "type": "object", + "properties": { + "@uri": { + "type": "string" + }, + "data": { + "description": "Post data", + "type": "object", + "properties": { + "name": { + "description": "Post tag", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "name": { + "description": "Post name", + "type": "string", + "example": "poti" + } + } + }, + "web.GenericListBody-types_Post": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + } + }, + "web.GenericListBodyMulti-types_Post-array_types_Post": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "meta": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + } + } + }, + "web.GenericListBodyMulti-types_Post-types_Post": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "meta": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + } + }, + "web.GenericListResponse-array_types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + }, + "status": { + "description": "Status of some other stuff", + "type": "string" + } + } + }, + "web.GenericListResponse-types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "status": { + "description": "Status of some other stuff", + "type": "string" + } + } + }, + "web.GenericListResponseMulti-types_Post-array_types_Post": { + "type": "object", + "properties": { + "itemsOne": { + "description": "ItemsOne is the first thing", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + }, + "status": { + "description": "Status of the things", + "type": "string" + } + } + }, + "web.GenericListResponseMulti-types_Post-types_Post": { + "type": "object", + "properties": { + "itemsOne": { + "description": "ItemsOne is the first thing", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "status": { + "description": "Status of the things", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/testdata/generics_arrays/main.go b/testdata/generics_arrays/main.go new file mode 100644 index 000000000..1e5423ecd --- /dev/null +++ b/testdata/generics_arrays/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/generics_arrays/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @host localhost:4000 +// @basePath /api +func main() { + http.HandleFunc("/posts/", api.GetPosts) + http.HandleFunc("/posts-multi/", api.GetPostMulti) + http.HandleFunc("/posts-multis/", api.GetPostArray) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/generics_arrays/types/post.go b/testdata/generics_arrays/types/post.go new file mode 100644 index 000000000..f4455f42f --- /dev/null +++ b/testdata/generics_arrays/types/post.go @@ -0,0 +1,17 @@ +package types + +type APIBase struct { + APIUrl string `json:"@uri,omitempty"` + ID int `json:"id" example:"1" format:"int64"` +} + +type Post struct { + APIBase + // Post name + Name string `json:"name" example:"poti"` + // Post data + Data struct { + // Post tag + Tag []string `json:"name"` + } `json:"data"` +} diff --git a/testdata/generics_arrays/web/handler.go b/testdata/generics_arrays/web/handler.go new file mode 100644 index 000000000..f0ac0f6b4 --- /dev/null +++ b/testdata/generics_arrays/web/handler.go @@ -0,0 +1,48 @@ +package web + +import ( + "time" +) + +type GenericListBody[T any] struct { + Data []T +} + +type GenericListBodyMulti[T any, X any] struct { + Data []T + Meta []X +} + +// GenericListResponse[T] +// @Description Some Generic List Response +type GenericListResponse[T any] struct { + // Items from the list response + Items []T + // Status of some other stuff + Status string +} + +// GenericListResponseMulti[T, X] +// @Description this contains a few things +type GenericListResponseMulti[T any, X any] struct { + // ItemsOne is the first thing + ItemsOne []T + // ItemsTwo is the second thing + ItemsTwo []X + + // Status of the things + Status string +} + +// APIError +// @Description API error +// @Description with information about it +// Other some summary +type APIError struct { + // Error an Api error + Error string // Error this is Line comment + // Error `number` tick comment + ErrorNo int64 + ErrorCtx string // Error `context` tick comment + CreatedAt time.Time // Error time +} diff --git a/testdata/generics_basic/.swaggo b/testdata/generics_basic/.swaggo new file mode 100644 index 000000000..30766d073 --- /dev/null +++ b/testdata/generics_basic/.swaggo @@ -0,0 +1,3 @@ +replace types.Field[string] string +replace types.DoubleField[string,string] []string +replace types.TrippleField[string,string] [][]string \ No newline at end of file diff --git a/testdata/generics_basic/api/api.go b/testdata/generics_basic/api/api.go new file mode 100644 index 000000000..381fe5446 --- /dev/null +++ b/testdata/generics_basic/api/api.go @@ -0,0 +1,68 @@ +package api + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/generics_basic/types" + "github.com/swaggo/swag/testdata/generics_basic/web" +) + +type Response[T any, X any] struct { + Data T + Meta X + + Status string +} + +type StringStruct struct { + Data string +} + +// @Summary Add a new pet to the store +// @Description get string by ID +// @Accept json +// @Produce json +// @Param data body web.GenericBody[types.Post] true "Some ID" +// @Success 200 {object} web.GenericResponse[types.Post] +// @Success 201 {object} web.GenericResponse[types.Hello] +// @Success 202 {object} web.GenericResponse[types.Field[string]] +// @Success 203 {object} web.GenericResponse[types.Field[int]] +// @Success 204 {object} Response[string, types.Field[int]] +// @Success 205 {object} Response[StringStruct, types.Field[int]] +// @Success 222 {object} web.GenericResponseMulti[types.Post, types.Post] +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 404 {object} web.APIError "Can not find ID" +// @Router /posts/ [post] +func GetPost(w http.ResponseWriter, r *http.Request) { + //write your code + _ = web.GenericResponse[types.Post]{} +} + +// @Summary Add new pets to the store +// @Description get string by ID +// @Accept json +// @Produce json +// @Param data body web.GenericBodyMulti[types.Post, types.Post] true "Some ID" +// @Success 200 {object} web.GenericResponse[types.Post] +// @Success 201 {object} web.GenericResponse[types.Hello] +// @Success 202 {object} web.GenericResponse[types.Field[string]] +// @Success 222 {object} web.GenericResponseMulti[types.Post, types.Post] +// @Router /posts-multi/ [post] +func GetPostMulti(w http.ResponseWriter, r *http.Request) { + //write your code + _ = web.GenericResponse[types.Post]{} +} + +// @Summary Add new pets to the store +// @Description get string by ID +// @Accept json +// @Produce json +// @Param data body web.GenericBodyMulti[[]types.Post, [][]types.Post] true "Some ID" +// @Success 200 {object} web.GenericResponse[[]types.Post] +// @Success 201 {object} web.GenericResponse[[]types.Hello] +// @Success 222 {object} web.GenericResponseMulti[[]types.Post, [][]types.Post] +// @Router /posts-multis/ [post] +func GetPostArray(w http.ResponseWriter, r *http.Request) { + //write your code + _ = web.GenericResponse[types.Post]{} +} diff --git a/testdata/generics_basic/expected.json b/testdata/generics_basic/expected.json new file mode 100644 index 000000000..b1d362497 --- /dev/null +++ b/testdata/generics_basic/expected.json @@ -0,0 +1,465 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server.", + "title": "Swagger Example API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:4000", + "basePath": "/api", + "paths": { + "/posts-multi/": { + "post": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add new pets to the store", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/web.GenericBodyMulti-types_Post-types_Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.GenericResponse-types_Post" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/web.GenericResponse-types_Hello" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/web.GenericResponse-types_Field-string" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/web.GenericResponseMulti-types_Post-types_Post" + } + } + } + } + }, + "/posts-multis/": { + "post": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add new pets to the store", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/web.GenericBodyMulti-array_types_Post-array2_types_Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.GenericResponse-array_types_Post" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/web.GenericResponse-array_types_Hello" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/web.GenericResponseMulti-array_types_Post-array2_types_Post" + } + } + } + } + }, + "/posts/": { + "post": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add a new pet to the store", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/web.GenericBody-types_Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.GenericResponse-types_Post" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/web.GenericResponse-types_Hello" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/web.GenericResponse-types_Field-string" + } + }, + "203": { + "description": "Non-Authoritative Information", + "schema": { + "$ref": "#/definitions/web.GenericResponse-types_Field-int" + } + }, + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/api.Response-string-types_Field-int" + } + }, + "205": { + "description": "Reset Content", + "schema": { + "$ref": "#/definitions/api.Response-api_StringStruct-types_Field-int" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/web.GenericResponseMulti-types_Post-types_Post" + } + }, + "400": { + "description": "We need ID!!", + "schema": { + "$ref": "#/definitions/web.APIError" + } + }, + "404": { + "description": "Can not find ID", + "schema": { + "$ref": "#/definitions/web.APIError" + } + } + } + } + } + }, + "definitions": { + "api.Response-api_StringStruct-types_Field-int": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api.StringStruct" + }, + "meta": { + "$ref": "#/definitions/types.Field-int" + }, + "status": { + "type": "string" + } + } + }, + "api.Response-string-types_Field-int": { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/types.Field-int" + }, + "status": { + "type": "string" + } + } + }, + "api.StringStruct": { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + }, + "types.Field-int": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + } + }, + "types.Field-string": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "types.Hello": { + "type": "object", + "properties": { + "myNewArrayField": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "myNewField": { + "type": "array", + "items": { + "type": "string" + } + }, + "myStringField1": { + "type": "string" + }, + "myStringField2": { + "type": "string" + } + } + }, + "types.Post": { + "type": "object", + "properties": { + "@uri": { + "type": "string" + }, + "data": { + "description": "Post data", + "type": "object", + "properties": { + "name": { + "description": "Post tag", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "name": { + "description": "Post name", + "type": "string", + "example": "poti" + } + } + }, + "web.APIError": { + "description": "API error with information about it", + "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.GenericBody-types_Post": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/types.Post" + } + } + }, + "web.GenericBodyMulti-array_types_Post-array2_types_Post": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "meta": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + } + } + }, + "web.GenericBodyMulti-types_Post-types_Post": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/types.Post" + }, + "meta": { + "$ref": "#/definitions/types.Post" + } + } + }, + "web.GenericResponse-array_types_Hello": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Hello" + } + }, + "status": { + "type": "string" + } + } + }, + "web.GenericResponse-array_types_Post": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "status": { + "type": "string" + } + } + }, + "web.GenericResponse-types_Field-int": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/types.Field-int" + }, + "status": { + "type": "string" + } + } + }, + "web.GenericResponse-types_Field-string": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/types.Field-string" + }, + "status": { + "type": "string" + } + } + }, + "web.GenericResponse-types_Hello": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/types.Hello" + }, + "status": { + "type": "string" + } + } + }, + "web.GenericResponse-types_Post": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/types.Post" + }, + "status": { + "type": "string" + } + } + }, + "web.GenericResponseMulti-array_types_Post-array2_types_Post": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "meta": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + }, + "status": { + "type": "string" + } + } + }, + "web.GenericResponseMulti-types_Post-types_Post": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/types.Post" + }, + "meta": { + "$ref": "#/definitions/types.Post" + }, + "status": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/testdata/generics_basic/main.go b/testdata/generics_basic/main.go new file mode 100644 index 000000000..99845ab89 --- /dev/null +++ b/testdata/generics_basic/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/generics_basic/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @host localhost:4000 +// @basePath /api +func main() { + http.HandleFunc("/posts/", api.GetPost) + http.HandleFunc("/posts-multi/", api.GetPostMulti) + http.HandleFunc("/posts-multis/", api.GetPostArray) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/generics_basic/types/post.go b/testdata/generics_basic/types/post.go new file mode 100644 index 000000000..de12dee31 --- /dev/null +++ b/testdata/generics_basic/types/post.go @@ -0,0 +1,18 @@ +package types + +type APIBase struct { + APIUrl string `json:"@uri,omitempty"` + ID int `json:"id" example:"1" format:"int64"` +} + +type Post struct { + APIBase + 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"` +} diff --git a/testdata/generics_basic/types/string.go b/testdata/generics_basic/types/string.go new file mode 100644 index 000000000..d90bcd1e7 --- /dev/null +++ b/testdata/generics_basic/types/string.go @@ -0,0 +1,22 @@ +package types + +type Field[T any] struct { + Value T +} + +type DoubleField[T1 any, T2 any] struct { + Value1 T1 + Value2 T2 +} + +type TrippleField[T1 any, T2 any] struct { + Value1 T1 + Value2 T2 +} + +type Hello struct { + MyStringField1 Field[*string] `json:"myStringField1"` + MyStringField2 Field[string] `json:"myStringField2"` + MyArrayField DoubleField[*string, string] `json:"myNewField"` + MyArrayDepthField TrippleField[*string, string] `json:"myNewArrayField"` +} diff --git a/testdata/generics_basic/web/handler.go b/testdata/generics_basic/web/handler.go new file mode 100644 index 000000000..39b7c7af5 --- /dev/null +++ b/testdata/generics_basic/web/handler.go @@ -0,0 +1,40 @@ +package web + +import ( + "time" +) + +type GenericBody[T any] struct { + Data T +} + +type GenericBodyMulti[T any, X any] struct { + Data T + Meta X +} + +type GenericResponse[T any] struct { + Data T + + Status string +} + +type GenericResponseMulti[T any, X any] struct { + Data T + Meta X + + Status string +} + +// APIError +// @Description API error +// @Description with information about it +// Other some summary +type APIError struct { + // Error an Api error + Error string // Error this is Line comment + // Error `number` tick comment + ErrorNo int64 + ErrorCtx string // Error `context` tick comment + CreatedAt time.Time // Error time +} diff --git a/testdata/generics_names/api/api.go b/testdata/generics_names/api/api.go new file mode 100644 index 000000000..7c30be9de --- /dev/null +++ b/testdata/generics_names/api/api.go @@ -0,0 +1,49 @@ +package api + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/generics_names/types" + "github.com/swaggo/swag/testdata/generics_names/web" +) + +// @Summary Add a new pet to the store +// @Description get string by ID +// @Accept json +// @Produce json +// @Param data body web.GenericBody[types.Post] true "Some ID" +// @Success 200 {object} web.GenericResponse[types.Post] +// @Success 222 {object} web.GenericResponseMulti[types.Post, types.Post] +// @Failure 400 {object} web.APIError "We need ID!!" +// @Failure 404 {object} web.APIError "Can not find ID" +// @Router /posts/ [post] +func GetPost(w http.ResponseWriter, r *http.Request) { + //write your code + _ = web.GenericResponse[types.Post]{} +} + +// @Summary Add new pets to the store +// @Description get string by ID +// @Accept json +// @Produce json +// @Param data body web.GenericBodyMulti[types.Post, types.Post] true "Some ID" +// @Success 200 {object} web.GenericResponse[types.Post] +// @Success 222 {object} web.GenericResponseMulti[types.Post, types.Post] +// @Router /posts-multi/ [post] +func GetPostMulti(w http.ResponseWriter, r *http.Request) { + //write your code + _ = web.GenericResponse[types.Post]{} +} + +// @Summary Add new pets to the store +// @Description get string by ID +// @Accept json +// @Produce json +// @Param data body web.GenericBodyMulti[[]types.Post, [][]types.Post] true "Some ID" +// @Success 200 {object} web.GenericResponse[[]types.Post] +// @Success 222 {object} web.GenericResponseMulti[[]types.Post, [][]types.Post] +// @Router /posts-multis/ [post] +func GetPostArray(w http.ResponseWriter, r *http.Request) { + //write your code + _ = web.GenericResponse[types.Post]{} +} diff --git a/testdata/generics_names/api/api_alias_pkg.go b/testdata/generics_names/api/api_alias_pkg.go new file mode 100644 index 000000000..2ae39d69a --- /dev/null +++ b/testdata/generics_names/api/api_alias_pkg.go @@ -0,0 +1,19 @@ +package api + +import ( + "net/http" + + mytypes "github.com/swaggo/swag/testdata/generics_names/types" + myweb "github.com/swaggo/swag/testdata/generics_names/web" +) + +// @Summary Add a new pet to the store +// @Description get string by ID +// @Accept json +// @Produce json +// @Success 200 {object} myweb.AliasPkgGenericResponse[mytypes.Post] +// @Router /posts/aliaspkg [post] +func GetPostFromAliasPkg(w http.ResponseWriter, r *http.Request) { + //write your code + _ = myweb.AliasPkgGenericResponse[mytypes.Post]{} +} diff --git a/testdata/generics_names/expected.json b/testdata/generics_names/expected.json new file mode 100644 index 000000000..741b2455d --- /dev/null +++ b/testdata/generics_names/expected.json @@ -0,0 +1,323 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server.", + "title": "Swagger Example API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:4000", + "basePath": "/api", + "paths": { + "/posts-multi/": { + "post": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add new pets to the store", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MultiBody-Post-Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Response-Post" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/MultiResponse-Post-Post" + } + } + } + } + }, + "/posts-multis/": { + "post": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add new pets to the store", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MultiBody-array_Post-array2_Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Response-array_Post" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/MultiResponse-array_Post-array2_Post" + } + } + } + } + }, + "/posts/": { + "post": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add a new pet to the store", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Body-Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Response-Post" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/MultiResponse-Post-Post" + } + }, + "400": { + "description": "We need ID!!", + "schema": { + "$ref": "#/definitions/web.APIError" + } + }, + "404": { + "description": "Can not find ID", + "schema": { + "$ref": "#/definitions/web.APIError" + } + } + } + } + }, + "/posts/aliaspkg": { + "post": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add a new pet to the store", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.AliasPkgGenericResponse-Post" + } + } + } + } + } + }, + "definitions": { + "Body-Post": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/Post" + } + } + }, + "MultiBody-Post-Post": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/Post" + }, + "meta": { + "$ref": "#/definitions/Post" + } + } + }, + "MultiBody-array_Post-array2_Post": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/Post" + } + }, + "meta": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Post" + } + } + } + } + }, + "MultiResponse-Post-Post": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/Post" + }, + "meta": { + "$ref": "#/definitions/Post" + }, + "status": { + "type": "string" + } + } + }, + "MultiResponse-array_Post-array2_Post": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/Post" + } + }, + "meta": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Post" + } + } + }, + "status": { + "type": "string" + } + } + }, + "Post": { + "type": "object", + "properties": { + "@uri": { + "type": "string" + }, + "data": { + "description": "Post data", + "type": "object", + "properties": { + "name": { + "description": "Post tag", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "name": { + "description": "Post name", + "type": "string", + "example": "poti" + } + } + }, + "Response-Post": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/Post" + }, + "status": { + "type": "string" + } + } + }, + "Response-array_Post": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/Post" + } + }, + "status": { + "type": "string" + } + } + }, + "web.APIError": { + "description": "API error with information about it", + "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.AliasPkgGenericResponse-Post": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/Post" + }, + "status": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/testdata/generics_names/main.go b/testdata/generics_names/main.go new file mode 100644 index 000000000..bf7307b9d --- /dev/null +++ b/testdata/generics_names/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/generics_names/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @host localhost:4000 +// @basePath /api +func main() { + http.HandleFunc("/posts/", api.GetPost) + http.HandleFunc("/posts-multi/", api.GetPostMulti) + http.HandleFunc("/posts-multis/", api.GetPostArray) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/generics_names/types/post.go b/testdata/generics_names/types/post.go new file mode 100644 index 000000000..4c57b9bfe --- /dev/null +++ b/testdata/generics_names/types/post.go @@ -0,0 +1,17 @@ +package types + +type APIBase struct { + APIUrl string `json:"@uri,omitempty"` + ID int `json:"id" example:"1" format:"int64"` +} + +type Post struct { + APIBase + // Post name + Name string `json:"name" example:"poti"` + // Post data + Data struct { + // Post tag + Tag []string `json:"name"` + } `json:"data"` +} // @name Post diff --git a/testdata/generics_names/web/handler.go b/testdata/generics_names/web/handler.go new file mode 100644 index 000000000..20c0a80d8 --- /dev/null +++ b/testdata/generics_names/web/handler.go @@ -0,0 +1,46 @@ +package web + +import ( + "time" +) + +type GenericBody[T any] struct { + Data T +} // @name Body + +type GenericBodyMulti[T any, X any] struct { + Data T + Meta X +} // @name MultiBody + +type GenericResponse[T any] struct { + Data T + + Status string +} // @name Response + +type GenericResponseMulti[T any, X any] struct { + Data T + Meta X + + Status string +} // @name MultiResponse + +// APIError +// @Description API error +// @Description with information about it +// Other some summary +type APIError struct { + // Error an Api error + Error string // Error this is Line comment + // Error `number` tick comment + ErrorNo int64 + ErrorCtx string // Error `context` tick comment + CreatedAt time.Time // Error time +} + +type AliasPkgGenericResponse[T any] struct { + Data T + + Status string +} diff --git a/testdata/generics_nested/api/api.go b/testdata/generics_nested/api/api.go new file mode 100644 index 000000000..76bc06b13 --- /dev/null +++ b/testdata/generics_nested/api/api.go @@ -0,0 +1,40 @@ +package api + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/generics_nested/types" + "github.com/swaggo/swag/testdata/generics_nested/web" +) + +// @Summary List Posts +// @Description Get All of the Posts +// @Accept json +// @Produce json +// @Param data body web.GenericNestedBody[web.GenericInnerType[types.Post]] true "Some ID" +// @Success 200 {object} web.GenericNestedResponse[types.Post] +// @Success 201 {object} web.GenericNestedResponse[web.GenericInnerType[types.Post]] +// @Success 202 {object} web.GenericNestedResponseMulti[types.Post, web.GenericInnerMultiType[types.Post, types.Post]] +// @Success 203 {object} web.GenericNestedResponseMulti[types.Post, web.GenericInnerMultiType[types.Post, web.GenericInnerType[types.Post]]] +// @Success 222 {object} web.GenericNestedResponseMulti[web.GenericInnerType[types.Post], types.Post] +// @Router /posts [get] +func GetPosts(w http.ResponseWriter, r *http.Request) { + _ = web.GenericNestedResponse[types.Post]{} +} + +// @Summary List Posts +// @Description Get All of the Posts +// @Accept json +// @Produce json +// @Param data body web.GenericNestedBody[web.GenericInnerType[[]types.Post]] true "Some ID" +// @Success 200 {object} web.GenericNestedResponse[[]types.Post] +// @Success 201 {object} web.GenericNestedResponse[[]web.GenericInnerType[types.Post]] +// @Success 202 {object} web.GenericNestedResponse[[]web.GenericInnerType[[]types.Post]] +// @Success 203 {object} web.GenericNestedResponseMulti[[]types.Post, web.GenericInnerMultiType[[]types.Post, types.Post]] +// @Success 204 {object} web.GenericNestedResponseMulti[[]types.Post, []web.GenericInnerMultiType[[]types.Post, types.Post]] +// @Success 205 {object} web.GenericNestedResponseMulti[types.Post, web.GenericInnerMultiType[types.Post, []web.GenericInnerType[[][]types.Post]]] +// @Success 222 {object} web.GenericNestedResponseMulti[web.GenericInnerType[[]types.Post], []types.Post] +// @Router /posts-multis/ [get] +func GetPostArray(w http.ResponseWriter, r *http.Request) { + _ = web.GenericNestedResponse[types.Post]{} +} diff --git a/testdata/generics_nested/expected.json b/testdata/generics_nested/expected.json new file mode 100644 index 000000000..850fc5ae4 --- /dev/null +++ b/testdata/generics_nested/expected.json @@ -0,0 +1,585 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server.", + "title": "Swagger Example API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:4000", + "basePath": "/api", + "paths": { + "/posts": { + "get": { + "description": "Get All of the Posts", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "List Posts", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/web.GenericNestedBody-web_GenericInnerType-types_Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponse-types_Post" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponse-web_GenericInnerType-types_Post" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponseMulti-types_Post-web_GenericInnerMultiType-types_Post-types_Post" + } + }, + "203": { + "description": "Non-Authoritative Information", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponseMulti-types_Post-web_GenericInnerMultiType-types_Post-web_GenericInnerType-types_Post" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponseMulti-web_GenericInnerType-types_Post-types_Post" + } + } + } + } + }, + "/posts-multis/": { + "get": { + "description": "Get All of the Posts", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "List Posts", + "parameters": [ + { + "description": "Some ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/web.GenericNestedBody-web_GenericInnerType-array_types_Post" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponse-array_types_Post" + } + }, + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponse-array_web_GenericInnerType-types_Post" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponse-array_web_GenericInnerType-array_types_Post" + } + }, + "203": { + "description": "Non-Authoritative Information", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponseMulti-array_types_Post-web_GenericInnerMultiType-array_types_Post-types_Post" + } + }, + "204": { + "description": "No Content", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponseMulti-array_types_Post-array_web_GenericInnerMultiType-array_types_Post-types_Post" + } + }, + "205": { + "description": "Reset Content", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponseMulti-types_Post-web_GenericInnerMultiType-types_Post-array_web_GenericInnerType-array2_types_Post" + } + }, + "222": { + "description": "", + "schema": { + "$ref": "#/definitions/web.GenericNestedResponseMulti-web_GenericInnerType-array_types_Post-array_types_Post" + } + } + } + } + } + }, + "definitions": { + "types.Post": { + "type": "object", + "properties": { + "@uri": { + "type": "string" + }, + "data": { + "description": "Post data", + "type": "object", + "properties": { + "name": { + "description": "Post tag", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "name": { + "description": "Post name", + "type": "string", + "example": "poti" + } + } + }, + "web.GenericInnerMultiType-array_types_Post-types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + } + }, + "web.GenericInnerMultiType-types_Post-array_web_GenericInnerType-array2_types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "allOf": [ + { + "$ref": "#/definitions/types.Post" + } + ] + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/web.GenericInnerType-array2_types_Post" + } + } + } + } + }, + "web.GenericInnerMultiType-types_Post-types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "allOf": [ + { + "$ref": "#/definitions/types.Post" + } + ] + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + } + }, + "web.GenericInnerMultiType-types_Post-web_GenericInnerType-types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "allOf": [ + { + "$ref": "#/definitions/types.Post" + } + ] + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "$ref": "#/definitions/web.GenericInnerType-types_Post" + } + } + } + }, + "web.GenericInnerType-array2_types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + } + } + }, + "web.GenericInnerType-array_types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + } + }, + "web.GenericInnerType-types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "allOf": [ + { + "$ref": "#/definitions/types.Post" + } + ] + } + } + }, + "web.GenericNestedBody-web_GenericInnerType-array_types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "allOf": [ + { + "$ref": "#/definitions/web.GenericInnerType-array_types_Post" + } + ] + }, + "status": { + "description": "Status of some other stuff", + "type": "string" + } + } + }, + "web.GenericNestedBody-web_GenericInnerType-types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "allOf": [ + { + "$ref": "#/definitions/web.GenericInnerType-types_Post" + } + ] + }, + "status": { + "description": "Status of some other stuff", + "type": "string" + } + } + }, + "web.GenericNestedResponse-array_types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + }, + "status": { + "description": "Status of some other stuff", + "type": "string" + } + } + }, + "web.GenericNestedResponse-array_web_GenericInnerType-array_types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/web.GenericInnerType-array_types_Post" + } + } + }, + "status": { + "description": "Status of some other stuff", + "type": "string" + } + } + }, + "web.GenericNestedResponse-array_web_GenericInnerType-types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/web.GenericInnerType-types_Post" + } + } + }, + "status": { + "description": "Status of some other stuff", + "type": "string" + } + } + }, + "web.GenericNestedResponse-types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "status": { + "description": "Status of some other stuff", + "type": "string" + } + } + }, + "web.GenericNestedResponse-web_GenericInnerType-types_Post": { + "type": "object", + "properties": { + "items": { + "description": "Items from the list response", + "type": "array", + "items": { + "$ref": "#/definitions/web.GenericInnerType-types_Post" + } + }, + "status": { + "description": "Status of some other stuff", + "type": "string" + } + } + }, + "web.GenericNestedResponseMulti-array_types_Post-array_web_GenericInnerMultiType-array_types_Post-types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/web.GenericInnerMultiType-array_types_Post-types_Post" + } + } + }, + "status": { + "description": "Status of the things", + "type": "string" + } + } + }, + "web.GenericNestedResponseMulti-array_types_Post-web_GenericInnerMultiType-array_types_Post-types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "$ref": "#/definitions/web.GenericInnerMultiType-array_types_Post-types_Post" + } + }, + "status": { + "description": "Status of the things", + "type": "string" + } + } + }, + "web.GenericNestedResponseMulti-types_Post-web_GenericInnerMultiType-types_Post-array_web_GenericInnerType-array2_types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "allOf": [ + { + "$ref": "#/definitions/types.Post" + } + ] + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "$ref": "#/definitions/web.GenericInnerMultiType-types_Post-array_web_GenericInnerType-array2_types_Post" + } + }, + "status": { + "description": "Status of the things", + "type": "string" + } + } + }, + "web.GenericNestedResponseMulti-types_Post-web_GenericInnerMultiType-types_Post-types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "allOf": [ + { + "$ref": "#/definitions/types.Post" + } + ] + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "$ref": "#/definitions/web.GenericInnerMultiType-types_Post-types_Post" + } + }, + "status": { + "description": "Status of the things", + "type": "string" + } + } + }, + "web.GenericNestedResponseMulti-types_Post-web_GenericInnerMultiType-types_Post-web_GenericInnerType-types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "allOf": [ + { + "$ref": "#/definitions/types.Post" + } + ] + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "$ref": "#/definitions/web.GenericInnerMultiType-types_Post-web_GenericInnerType-types_Post" + } + }, + "status": { + "description": "Status of the things", + "type": "string" + } + } + }, + "web.GenericNestedResponseMulti-web_GenericInnerType-array_types_Post-array_types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "allOf": [ + { + "$ref": "#/definitions/web.GenericInnerType-array_types_Post" + } + ] + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + }, + "status": { + "description": "Status of the things", + "type": "string" + } + } + }, + "web.GenericNestedResponseMulti-web_GenericInnerType-types_Post-types_Post": { + "type": "object", + "properties": { + "itemOne": { + "description": "ItemsOne is the first thing", + "allOf": [ + { + "$ref": "#/definitions/web.GenericInnerType-types_Post" + } + ] + }, + "itemsTwo": { + "description": "ItemsTwo is the second thing", + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "status": { + "description": "Status of the things", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/testdata/generics_nested/main.go b/testdata/generics_nested/main.go new file mode 100644 index 000000000..4817e03cb --- /dev/null +++ b/testdata/generics_nested/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/generics_nested/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @host localhost:4000 +// @basePath /api +func main() { + http.HandleFunc("/posts/", api.GetPosts) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/generics_nested/types/post.go b/testdata/generics_nested/types/post.go new file mode 100644 index 000000000..f4455f42f --- /dev/null +++ b/testdata/generics_nested/types/post.go @@ -0,0 +1,17 @@ +package types + +type APIBase struct { + APIUrl string `json:"@uri,omitempty"` + ID int `json:"id" example:"1" format:"int64"` +} + +type Post struct { + APIBase + // Post name + Name string `json:"name" example:"poti"` + // Post data + Data struct { + // Post tag + Tag []string `json:"name"` + } `json:"data"` +} diff --git a/testdata/generics_nested/web/handler.go b/testdata/generics_nested/web/handler.go new file mode 100644 index 000000000..9d53e91d4 --- /dev/null +++ b/testdata/generics_nested/web/handler.go @@ -0,0 +1,64 @@ +package web + +import ( + "time" +) + +// GenericNestedBody[T] +// @Description Some Generic Body +type GenericNestedBody[T any] struct { + // Items from the list response + Items T + // Status of some other stuff + Status string +} + +// GenericInnerType[T] +// @Description Some Generic Body +type GenericInnerType[T any] struct { + // Items from the list response + Items T +} + +// GenericInnerMultiType[T, X] +// @Description Some Generic Body +type GenericInnerMultiType[T any, X any] struct { + // ItemsOne is the first thing + ItemOne T + // ItemsTwo is the second thing + ItemsTwo []X +} + +// GenericNestedResponse[T] +// @Description Some Generic List Response +type GenericNestedResponse[T any] struct { + // Items from the list response + Items []T + // Status of some other stuff + Status string +} + +// GenericNestedResponseMulti[T, X] +// @Description this contains a few things +type GenericNestedResponseMulti[T any, X any] struct { + // ItemsOne is the first thing + ItemOne T + // ItemsTwo is the second thing + ItemsTwo []X + + // Status of the things + Status string +} + +// APIError +// @Description API error +// @Description with information about it +// Other some summary +type APIError struct { + // Error an Api error + Error string // Error this is Line comment + // Error `number` tick comment + ErrorNo int64 + ErrorCtx string // Error `context` tick comment + CreatedAt time.Time // Error time +} diff --git a/testdata/generics_package_alias/external/external1/external.go b/testdata/generics_package_alias/external/external1/external.go new file mode 100644 index 000000000..58e0848e6 --- /dev/null +++ b/testdata/generics_package_alias/external/external1/external.go @@ -0,0 +1,6 @@ +package external1 + +type Customer struct { + Name string + Age int +} diff --git a/testdata/generics_package_alias/external/external2/external.go b/testdata/generics_package_alias/external/external2/external.go new file mode 100644 index 000000000..2a0771c6e --- /dev/null +++ b/testdata/generics_package_alias/external/external2/external.go @@ -0,0 +1,6 @@ +package external2 + +type Customer struct { + Name string + Age int +} diff --git a/testdata/generics_package_alias/external/external3/external.go b/testdata/generics_package_alias/external/external3/external.go new file mode 100644 index 000000000..a70e5ccc6 --- /dev/null +++ b/testdata/generics_package_alias/external/external3/external.go @@ -0,0 +1,6 @@ +package external3 + +type Customer struct { + Name string + Age int +} diff --git a/testdata/generics_package_alias/external/external4/external.go b/testdata/generics_package_alias/external/external4/external.go new file mode 100644 index 000000000..2d050bde9 --- /dev/null +++ b/testdata/generics_package_alias/external/external4/external.go @@ -0,0 +1,6 @@ +package external4 + +type Customer struct { + Name string + Age int +} diff --git a/testdata/generics_package_alias/internal/api/api1.go b/testdata/generics_package_alias/internal/api/api1.go new file mode 100644 index 000000000..4152ab313 --- /dev/null +++ b/testdata/generics_package_alias/internal/api/api1.go @@ -0,0 +1,25 @@ +package api + +import ( + myv1 "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" +) + +// @Summary Create movie +// @Description Create a new movie production +// @Accept json +// @Produce json +// @Success 200 {object} myv1.ListResult[myv1.ProductDto] "" +// @Router /api01 [post] +func CreateMovie01() { + _ = myv1.ListResult[myv1.ProductDto]{} +} + +// @Summary Create movie +// @Description Create a new movie production +// @Accept json +// @Produce json +// @Success 200 {object} myv1.RenamedListResult[myv1.RenamedProductDto] "" +// @Router /api02 [post] +func CreateMovie02() { + _ = myv1.ListResult[myv1.ProductDto]{} +} diff --git a/testdata/generics_package_alias/internal/api/api2.go b/testdata/generics_package_alias/internal/api/api2.go new file mode 100644 index 000000000..d0cb09610 --- /dev/null +++ b/testdata/generics_package_alias/internal/api/api2.go @@ -0,0 +1,46 @@ +package api + +import ( + myv1 "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" + myv2 "github.com/swaggo/swag/testdata/generics_package_alias/internal/path2/v1" +) + +// @Summary Create movie +// @Description Create a new movie production +// @Accept json +// @Produce json +// @Success 200 {object} myv2.ListResult[myv2.ProductDto] "" +// @Router /api03 [post] +func CreateMovie03() { + _ = myv2.ListResult[myv2.ProductDto]{} +} + +// @Summary Create movie +// @Description Create a new movie production +// @Accept json +// @Produce json +// @Success 200 {object} myv2.RenamedListResult[myv2.RenamedProductDto] "" +// @Router /api04 [post] +func CreateMovie04() { + _ = myv2.ListResult[myv2.ProductDto]{} +} + +// @Summary Create movie +// @Description Create a new movie production +// @Accept json +// @Produce json +// @Success 200 {object} myv1.ListResult[myv2.ProductDto] "" +// @Router /api05 [post] +func CreateMovie05() { + _ = myv1.ListResult[myv2.ProductDto]{} +} + +// @Summary Create movie +// @Description Create a new movie production +// @Accept json +// @Produce json +// @Success 200 {object} myv1.RenamedListResult[myv2.RenamedProductDto] "" +// @Router /api06 [post] +func CreateMovie06() { + _ = myv1.ListResult[myv2.ProductDto]{} +} diff --git a/testdata/generics_package_alias/internal/api/api3.go b/testdata/generics_package_alias/internal/api/api3.go new file mode 100644 index 000000000..f269d734f --- /dev/null +++ b/testdata/generics_package_alias/internal/api/api3.go @@ -0,0 +1,46 @@ +package api + +import ( + _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" + . "github.com/swaggo/swag/testdata/generics_package_alias/internal/path2/v1" +) + +// @Summary Create movie +// @Description models imported from an unnamed package +// @Accept json +// @Produce json +// @Success 200 {object} v1.ListResult[v1.ProductDto] "" +// @Router /api07 [post] +func CreateMovie07() { + var _ ProductDto +} + +// @Summary Create movie +// @Description models imported from an unnamed package +// @Accept json +// @Produce json +// @Success 200 {object} ListResult[ProductDto] "" +// @Router /api08 [post] +func CreateMovie08() { + var _ ProductDto +} + +// @Summary Create movie +// @Description models imported from an unnamed package +// @Accept json +// @Produce json +// @Success 200 {object} ListResult[v1.ProductDto] "" +// @Router /api09 [post] +func CreateMovie09() { + var _ ProductDto +} + +// @Summary Create movie +// @Description models imported from an unnamed package +// @Accept json +// @Produce json +// @Success 200 {object} v1.ListResult[ProductDto] "" +// @Router /api10 [post] +func CreateMovie10() { + var _ ProductDto +} diff --git a/testdata/generics_package_alias/internal/api/api4.go b/testdata/generics_package_alias/internal/api/api4.go new file mode 100644 index 000000000..9a851ba04 --- /dev/null +++ b/testdata/generics_package_alias/internal/api/api4.go @@ -0,0 +1,16 @@ +package api + +import ( + "github.com/swaggo/swag/testdata/generics_package_alias/external/external1" + _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" +) + +// @Summary Create movie +// @Description models imported from an external package +// @Accept json +// @Produce json +// @Success 200 {object} v1.ListResult[external1.Customer] "" +// @Router /api11 [post] +func CreateMovie11() { + var _ external1.Customer +} diff --git a/testdata/generics_package_alias/internal/api/api5.go b/testdata/generics_package_alias/internal/api/api5.go new file mode 100644 index 000000000..c15272709 --- /dev/null +++ b/testdata/generics_package_alias/internal/api/api5.go @@ -0,0 +1,16 @@ +package api + +import ( + myexternal "github.com/swaggo/swag/testdata/generics_package_alias/external/external2" + _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" +) + +// @Summary Create movie +// @Description models imported from a named external package +// @Accept json +// @Produce json +// @Success 200 {object} v1.ListResult[myexternal.Customer] "" +// @Router /api12 [post] +func CreateMovie12() { + var _ myexternal.Customer +} diff --git a/testdata/generics_package_alias/internal/api/api6.go b/testdata/generics_package_alias/internal/api/api6.go new file mode 100644 index 000000000..5d1d27072 --- /dev/null +++ b/testdata/generics_package_alias/internal/api/api6.go @@ -0,0 +1,16 @@ +package api + +import ( + . "github.com/swaggo/swag/testdata/generics_package_alias/external/external3" + _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" +) + +// @Summary Create movie +// @Description models from an external package imported by mode dot +// @Accept json +// @Produce json +// @Success 200 {object} v1.ListResult[Customer] "" +// @Router /api13 [post] +func CreateMovie13() { + var _ Customer +} diff --git a/testdata/generics_package_alias/internal/api/api7.go b/testdata/generics_package_alias/internal/api/api7.go new file mode 100644 index 000000000..5a5ad72c4 --- /dev/null +++ b/testdata/generics_package_alias/internal/api/api7.go @@ -0,0 +1,16 @@ +package api + +import ( + _ "github.com/swaggo/swag/testdata/generics_package_alias/external/external4" + _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" +) + +// @Summary Create movie +// @Description models imported from an unnamed external package +// @Accept json +// @Produce json +// @Success 200 {object} v1.ListResult[external4.Customer] "" +// @Router /api14 [post] +func CreateMovie14() { + +} diff --git a/testdata/generics_package_alias/internal/api/api8.go b/testdata/generics_package_alias/internal/api/api8.go new file mode 100644 index 000000000..bcdd94715 --- /dev/null +++ b/testdata/generics_package_alias/internal/api/api8.go @@ -0,0 +1,16 @@ +package api + +import ( + _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path1/v1" + _ "github.com/swaggo/swag/testdata/generics_package_alias/internal/path2/v1" +) + +// @Summary Create movie +// @Description model from a package whose name conflicts with other packages +// @Accept json +// @Produce json +// @Success 200 {object} v1.UniqueProduct "" +// @Router /api15 [post] +func CreateMovie15() { + +} diff --git a/testdata/generics_package_alias/internal/expected.json b/testdata/generics_package_alias/internal/expected.json new file mode 100644 index 000000000..2f6c85f52 --- /dev/null +++ b/testdata/generics_package_alias/internal/expected.json @@ -0,0 +1,515 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/api01": { + "post": { + "description": "Create a new movie production", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1_ProductDto" + } + } + } + } + }, + "/api02": { + "post": { + "description": "Create a new movie production", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/ListResultV1-ProductDtoV1" + } + } + } + } + }, + "/api03": { + "post": { + "description": "Create a new movie production", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto" + } + } + } + } + }, + "/api04": { + "post": { + "description": "Create a new movie production", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/ListResultV2-ProductDtoV2" + } + } + } + } + }, + "/api05": { + "post": { + "description": "Create a new movie production", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto" + } + } + } + } + }, + "/api06": { + "post": { + "description": "Create a new movie production", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/ListResultV1-ProductDtoV2" + } + } + } + } + }, + "/api07": { + "post": { + "description": "models imported from an unnamed package", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1_ProductDto" + } + } + } + } + }, + "/api08": { + "post": { + "description": "models imported from an unnamed package", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto" + } + } + } + } + }, + "/api09": { + "post": { + "description": "models imported from an unnamed package", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1_ProductDto" + } + } + } + } + }, + "/api10": { + "post": { + "description": "models imported from an unnamed package", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto" + } + } + } + } + }, + "/api11": { + "post": { + "description": "models imported from an external package", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external1_Customer" + } + } + } + } + }, + "/api12": { + "post": { + "description": "models imported from a named external package", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external2_Customer" + } + } + } + } + }, + "/api13": { + "post": { + "description": "models from an external package imported by mode dot", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external3_Customer" + } + } + } + } + }, + "/api14": { + "post": { + "description": "models imported from an unnamed external package", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external4_Customer" + } + } + } + } + }, + "/api15": { + "post": { + "description": "model from a package whose name conflicts with other packages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.UniqueProduct" + } + } + } + } + } + }, + "definitions": { + "ListResultV1-ProductDtoV1": { + "type": "object", + "properties": { + "items11": { + "type": "array", + "items": { + "$ref": "#/definitions/ProductDtoV1" + } + } + } + }, + "ListResultV1-ProductDtoV2": { + "type": "object", + "properties": { + "items11": { + "type": "array", + "items": { + "$ref": "#/definitions/ProductDtoV2" + } + } + } + }, + "ListResultV2-ProductDtoV2": { + "type": "object", + "properties": { + "items22": { + "type": "array", + "items": { + "$ref": "#/definitions/ProductDtoV2" + } + } + } + }, + "ProductDtoV1": { + "type": "object", + "properties": { + "name11": { + "type": "string" + } + } + }, + "ProductDtoV2": { + "type": "object", + "properties": { + "name22": { + "type": "string" + } + } + }, + "external1.Customer": { + "type": "object", + "properties": { + "age": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "external2.Customer": { + "type": "object", + "properties": { + "age": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "external3.Customer": { + "type": "object", + "properties": { + "age": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "external4.Customer": { + "type": "object", + "properties": { + "age": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external1_Customer": { + "type": "object", + "properties": { + "items1": { + "type": "array", + "items": { + "$ref": "#/definitions/external1.Customer" + } + } + } + }, + "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external2_Customer": { + "type": "object", + "properties": { + "items1": { + "type": "array", + "items": { + "$ref": "#/definitions/external2.Customer" + } + } + } + }, + "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external3_Customer": { + "type": "object", + "properties": { + "items1": { + "type": "array", + "items": { + "$ref": "#/definitions/external3.Customer" + } + } + } + }, + "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-external4_Customer": { + "type": "object", + "properties": { + "items1": { + "type": "array", + "items": { + "$ref": "#/definitions/external4.Customer" + } + } + } + }, + "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1_ProductDto": { + "type": "object", + "properties": { + "items1": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ProductDto" + } + } + } + }, + "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto": { + "type": "object", + "properties": { + "items1": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ProductDto" + } + } + } + }, + "github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ProductDto": { + "type": "object", + "properties": { + "name1": { + "type": "string" + } + } + }, + "github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1_ProductDto": { + "type": "object", + "properties": { + "items2": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path1_v1.ProductDto" + } + } + } + }, + "github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ListResult-github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1_ProductDto": { + "type": "object", + "properties": { + "items2": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ProductDto" + } + } + } + }, + "github_com_swaggo_swag_testdata_generics_package_alias_internal_path2_v1.ProductDto": { + "type": "object", + "properties": { + "name2": { + "type": "string" + } + } + }, + "v1.UniqueProduct": { + "type": "object", + "properties": { + "unique_product_name": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/testdata/generics_package_alias/internal/main.go b/testdata/generics_package_alias/internal/main.go new file mode 100644 index 000000000..790580777 --- /dev/null +++ b/testdata/generics_package_alias/internal/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/testdata/generics_package_alias/internal/path1/v1/product.go b/testdata/generics_package_alias/internal/path1/v1/product.go new file mode 100644 index 000000000..c0524a37b --- /dev/null +++ b/testdata/generics_package_alias/internal/path1/v1/product.go @@ -0,0 +1,17 @@ +package v1 + +type ProductDto struct { + Name1 string `json:"name1"` +} + +type ListResult[T any] struct { + Items1 []T `json:"items1,omitempty"` +} + +type RenamedProductDto struct { + Name11 string `json:"name11"` +} // @name ProductDtoV1 + +type RenamedListResult[T any] struct { + Items11 []T `json:"items11,omitempty"` +} // @name ListResultV1 diff --git a/testdata/generics_package_alias/internal/path2/v1/product.go b/testdata/generics_package_alias/internal/path2/v1/product.go new file mode 100644 index 000000000..14a1b1107 --- /dev/null +++ b/testdata/generics_package_alias/internal/path2/v1/product.go @@ -0,0 +1,21 @@ +package v1 + +type ProductDto struct { + Name2 string `json:"name2"` +} + +type ListResult[T any] struct { + Items2 []T `json:"items2,omitempty"` +} + +type RenamedProductDto struct { + Name22 string `json:"name22"` +} // @name ProductDtoV2 + +type RenamedListResult[T any] struct { + Items22 []T `json:"items22,omitempty"` +} // @name ListResultV2 + +type UniqueProduct struct { + UniqueProductName string `json:"unique_product_name"` +} diff --git a/testdata/generics_package_alias/internal/path3/v1/product.go b/testdata/generics_package_alias/internal/path3/v1/product.go new file mode 100644 index 000000000..be5e25127 --- /dev/null +++ b/testdata/generics_package_alias/internal/path3/v1/product.go @@ -0,0 +1,17 @@ +package v1 + +type ProductDto struct { + Name3 string `json:"name3"` +} + +type ListResult[T any] struct { + Items3 []T `json:"items3,omitempty"` +} + +type RenamedProductDto struct { + Name33 string `json:"name33"` +} // @name ProductDtoV3 + +type RenamedListResult[T any] struct { + Items33 []T `json:"items33,omitempty"` +} // @name ListResultV3 diff --git a/testdata/generics_property/api/api.go b/testdata/generics_property/api/api.go new file mode 100644 index 000000000..e68938f81 --- /dev/null +++ b/testdata/generics_property/api/api.go @@ -0,0 +1,54 @@ +package api + +import ( + "github.com/swaggo/swag/testdata/generics_property/types" + "github.com/swaggo/swag/testdata/generics_property/web" + "net/http" +) + +type NestedResponse struct { + web.GenericResponse[[]string, *uint8] + Post types.Field[[]types.Post] +} + +type Audience[T any] []T + +type CreateMovie struct { + Name string + MainActor types.Field[Person] + SupportingCast types.Field[[]Person] + Directors types.Field[*[]Person] + CameraPeople types.Field[[]*Person] + Producer types.Field[*Person] + Audience Audience[Person] + AudienceNames Audience[string] + Detail1 types.Field[types.Field[Person]] + Detail2 types.Field[types.Field[string]] +} + +type Person struct { + Name string +} + +// @Summary List Posts +// @Description Get All of the Posts +// @Accept json +// @Produce json +// @Param data query web.PostPager true "1" +// @Success 200 {object} web.PostResponse "ok" +// @Success 201 {object} web.PostResponses "ok" +// @Success 202 {object} web.StringResponse "ok" +// @Success 203 {object} NestedResponse "ok" +// @Router /posts [get] +func GetPosts(w http.ResponseWriter, r *http.Request) { +} + +// @Summary Create movie +// @Description Create a new movie production +// @Accept json +// @Produce json +// @Param data body CreateMovie true "Movie Create-Payload" +// @Success 201 {object} CreateMovie "ok" +// @Router /movie [post] +func CreateMovieApi(w http.ResponseWriter, r *http.Request) { +} diff --git a/testdata/generics_property/expected.json b/testdata/generics_property/expected.json new file mode 100644 index 000000000..e0880258f --- /dev/null +++ b/testdata/generics_property/expected.json @@ -0,0 +1,428 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server.", + "title": "Swagger Example API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:4000", + "basePath": "/api", + "paths": { + "/movie": { + "post": { + "description": "Create a new movie production", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create movie", + "parameters": [ + { + "description": "Movie Create-Payload", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.CreateMovie" + } + } + ], + "responses": { + "201": { + "description": "ok", + "schema": { + "$ref": "#/definitions/api.CreateMovie" + } + } + } + } + }, + "/posts": { + "get": { + "description": "Get All of the Posts", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "List Posts", + "parameters": [ + { + "type": "string", + "name": "next_id", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "string", + "name": "prev_id", + "in": "query" + }, + { + "type": "integer", + "name": "rows", + "in": "query" + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.PostResponse" + } + }, + "201": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.PostResponses" + } + }, + "202": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.StringResponse" + } + }, + "203": { + "description": "ok", + "schema": { + "$ref": "#/definitions/api.NestedResponse" + } + } + } + } + } + }, + "definitions": { + "api.CreateMovie": { + "type": "object", + "properties": { + "audience": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Person" + } + }, + "audienceNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "cameraPeople": { + "$ref": "#/definitions/types.Field-array_api_Person" + }, + "detail1": { + "$ref": "#/definitions/types.Field-types_Field-api_Person" + }, + "detail2": { + "$ref": "#/definitions/types.Field-types_Field-string" + }, + "directors": { + "$ref": "#/definitions/types.Field-array_api_Person" + }, + "mainActor": { + "$ref": "#/definitions/types.Field-api_Person" + }, + "name": { + "type": "string" + }, + "producer": { + "$ref": "#/definitions/types.Field-api_Person" + }, + "supportingCast": { + "$ref": "#/definitions/types.Field-array_api_Person" + } + } + }, + "api.NestedResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + }, + "items2": { + "type": "integer" + }, + "post": { + "$ref": "#/definitions/types.Field-array_types_Post" + } + } + }, + "api.Person": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "types.Field-api_Person": { + "type": "object", + "properties": { + "value": { + "$ref": "#/definitions/api.Person" + }, + "value2": { + "$ref": "#/definitions/api.Person" + }, + "value3": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Person" + } + }, + "value4": { + "type": "object", + "properties": { + "subValue1": { + "$ref": "#/definitions/api.Person" + }, + "subValue2": { + "type": "string" + } + } + } + } + }, + "types.Field-array_api_Person": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Person" + } + }, + "value2": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Person" + } + }, + "value3": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/api.Person" + } + } + }, + "value4": { + "type": "object", + "properties": { + "subValue1": { + "$ref": "#/definitions/api.Person" + }, + "subValue2": { + "type": "string" + } + } + } + } + }, + "types.Field-array_types_Post": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "value2": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "value3": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + } + }, + "value4": { + "type": "object", + "properties": { + "subValue1": { + "$ref": "#/definitions/types.Post" + }, + "subValue2": { + "type": "string" + } + } + } + } + }, + "types.Field-string": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "value2": { + "type": "string" + }, + "value3": { + "type": "array", + "items": { + "type": "string" + } + }, + "value4": { + "type": "object", + "properties": { + "subValue1": { + "type": "string" + }, + "subValue2": { + "type": "string" + } + } + } + } + }, + "types.Field-types_Field-api_Person": { + "type": "object", + "properties": { + "value": { + "$ref": "#/definitions/types.Field-api_Person" + }, + "value2": { + "$ref": "#/definitions/types.Field-api_Person" + }, + "value3": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Field-api_Person" + } + }, + "value4": { + "type": "object", + "properties": { + "subValue1": { + "$ref": "#/definitions/types.Field-api_Person" + }, + "subValue2": { + "type": "string" + } + } + } + } + }, + "types.Field-types_Field-string": { + "type": "object", + "properties": { + "value": { + "$ref": "#/definitions/types.Field-string" + }, + "value2": { + "$ref": "#/definitions/types.Field-string" + }, + "value3": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Field-string" + } + }, + "value4": { + "type": "object", + "properties": { + "subValue1": { + "$ref": "#/definitions/types.Field-string" + }, + "subValue2": { + "type": "string" + } + } + } + } + }, + "types.Post": { + "type": "object", + "properties": { + "@uri": { + "$ref": "#/definitions/types.Field-string" + }, + "data": { + "description": "Post data", + "type": "object", + "properties": { + "name": { + "description": "Post tag", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "name": { + "description": "Post name", + "type": "string", + "example": "poti" + } + } + }, + "web.PostResponse": { + "type": "object", + "properties": { + "items": { + "$ref": "#/definitions/types.Post" + }, + "items2": { + "$ref": "#/definitions/types.Post" + } + } + }, + "web.PostResponses": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Post" + } + }, + "items2": { + "$ref": "#/definitions/types.Post" + } + } + }, + "web.StringResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + }, + "items2": { + "type": "integer" + } + } + } + } +} \ No newline at end of file diff --git a/testdata/generics_property/main.go b/testdata/generics_property/main.go new file mode 100644 index 000000000..41b58c6e3 --- /dev/null +++ b/testdata/generics_property/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "net/http" + + "github.com/swaggo/swag/testdata/generics_property/api" +) + +// @title Swagger Example API +// @version 1.0 +// @description This is a sample server Petstore server. +// @host localhost:4000 +// @basePath /api +func main() { + http.HandleFunc("/posts/", api.GetPosts) + http.HandleFunc("/movie/", api.CreateMovieApi) + http.ListenAndServe(":8080", nil) +} diff --git a/testdata/generics_property/types/post.go b/testdata/generics_property/types/post.go new file mode 100644 index 000000000..732863e79 --- /dev/null +++ b/testdata/generics_property/types/post.go @@ -0,0 +1,29 @@ +package types + +type SubField1[T any, T2 any] struct { + SubValue1 T + SubValue2 T2 +} + +type Field[T any] struct { + Value T + Value2 *T + Value3 []T + Value4 SubField1[T, string] +} + +type APIBase struct { + APIUrl Field[string] `json:"@uri,omitempty"` + ID int `json:"id" example:"1" format:"int64"` +} + +type Post struct { + APIBase + // Post name + Name string `json:"name" example:"poti"` + // Post data + Data struct { + // Post tag + Tag []string `json:"name"` + } `json:"data"` +} diff --git a/testdata/generics_property/web/handler.go b/testdata/generics_property/web/handler.go new file mode 100644 index 000000000..e22aef025 --- /dev/null +++ b/testdata/generics_property/web/handler.go @@ -0,0 +1,49 @@ +package web + +import "github.com/swaggo/swag/testdata/generics_property/types" + +type PostSelector func(selector func()) + +type Filter interface { + ~func(selector func()) +} + +type query[T any, F Filter] interface { + Where(ps ...F) T +} + +type Pager[T query[T, F], F Filter] struct { + Rows uint8 `json:"rows" form:"rows"` + Page int `json:"page" form:"page"` + NextID *string `json:"next_id" form:"next_id"` + PrevID *string `json:"prev_id" form:"prev_id"` + query T +} + +type String string + +func (String) Where(ps ...PostSelector) String { + return "" +} + +type PostPager struct { + Pager[String, PostSelector] + Search types.Field[string] `json:"search" form:"search"` +} + +type PostResponse struct { + GenericResponse[types.Post, types.Post] +} + +type PostResponses struct { + GenericResponse[[]types.Post, types.Post] +} + +type StringResponse struct { + GenericResponse[[]string, *uint8] +} + +type GenericResponse[T any, T2 any] struct { + Items T + Items2 T2 +} diff --git a/testdata/simple/api/api.go b/testdata/simple/api/api.go index 34e843713..85a7fa48f 100644 --- a/testdata/simple/api/api.go +++ b/testdata/simple/api/api.go @@ -130,3 +130,11 @@ type SwagReturn []map[string]string func GetPet6MapString() { } + +// @Success 200 {object} api.GetPet6FunctionScopedResponse.response "ok" +// @Router /GetPet6FunctionScopedResponse [get] +func GetPet6FunctionScopedResponse() { + type response struct { + Name string + } +} diff --git a/testdata/simple/expected.json b/testdata/simple/expected.json index e3190a02b..24ce9f696 100644 --- a/testdata/simple/expected.json +++ b/testdata/simple/expected.json @@ -101,6 +101,18 @@ } } }, + "/GetPet6FunctionScopedResponse": { + "get": { + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/api.GetPet6FunctionScopedResponse.response" + } + } + } + } + }, "/GetPet6MapString": { "get": { "responses": { @@ -203,17 +215,17 @@ } } }, - "404": { - "description": "Can not find ID", - "schema": { - "$ref": "#/definitions/web.APIError" - } - }, "403": { "description": "cross", "schema": { "$ref": "#/definitions/cross.Cross" } + }, + "404": { + "description": "Can not find ID", + "schema": { + "$ref": "#/definitions/web.APIError" + } } } } @@ -389,6 +401,14 @@ } }, "definitions": { + "api.GetPet6FunctionScopedResponse.response": { + "type": "object", + "properties": { + "Name": { + "type": "string" + } + } + }, "cross.Cross": { "type": "object", "properties": { @@ -702,12 +722,6 @@ "Data": { "type": "integer" }, - "Err": { - "type": "integer" - }, - "Status": { - "type": "boolean" - }, "cross": { "$ref": "#/definitions/cross.Cross" }, @@ -716,6 +730,20 @@ "items": { "$ref": "#/definitions/cross.Cross" } + }, + "rev_value_base": { + "$ref": "#/definitions/web.RevValueBase" + } + } + }, + "web.RevValueBase": { + "type": "object", + "properties": { + "Err": { + "type": "integer" + }, + "Status": { + "type": "boolean" } } }, @@ -785,4 +813,4 @@ } } } -} +} \ No newline at end of file diff --git a/types.go b/types.go index 82ddbbebb..79c0f6a0d 100644 --- a/types.go +++ b/types.go @@ -2,6 +2,8 @@ package swag import ( "go/ast" + "go/token" + "strings" "github.com/go-openapi/spec" ) @@ -21,8 +23,13 @@ type TypeSpecDef struct { // the TypeSpec of this type definition TypeSpec *ast.TypeSpec + Enums []EnumValue + // path of package starting from under ${GOPATH}/src or from module path in go.mod - PkgPath string + PkgPath string + ParentSpec ast.Decl + + NotUnique bool } // Name the name of the typeSpec. @@ -34,18 +41,49 @@ func (t *TypeSpecDef) Name() string { return "" } -// FullName full name of the typeSpec. -func (t *TypeSpecDef) FullName() string { - return fullTypeName(t.File.Name.Name, t.TypeSpec.Name.Name) +// TypeName the type name of the typeSpec. +func (t *TypeSpecDef) TypeName() string { + if ignoreNameOverride(t.TypeSpec.Name.Name) { + return t.TypeSpec.Name.Name[1:] + } else if t.TypeSpec.Comment != nil { + // get alias from comment '// @name ' + for _, comment := range t.TypeSpec.Comment.List { + texts := strings.Split(strings.TrimSpace(strings.TrimLeft(comment.Text, "/")), " ") + if len(texts) > 1 && strings.ToLower(texts[0]) == "@name" { + return texts[1] + } + } + } + + var names []string + if t.NotUnique { + pkgPath := strings.Map(func(r rune) rune { + if r == '\\' || r == '/' || r == '.' { + return '_' + } + return r + }, t.PkgPath) + names = append(names, pkgPath) + } else { + names = append(names, t.File.Name.Name) + } + if parentFun, ok := (t.ParentSpec).(*ast.FuncDecl); ok && parentFun != nil { + names = append(names, parentFun.Name.Name) + } + names = append(names, t.TypeSpec.Name.Name) + return fullTypeName(names...) } -// FullPath of the typeSpec. +// FullPath return the full path of the typeSpec. func (t *TypeSpecDef) FullPath() string { return t.PkgPath + "." + t.Name() } // AstFileInfo information of an ast.File. type AstFileInfo struct { + //FileSet the FileSet object which is used to parse this go source file + FileSet *token.FileSet + // File ast.File File *ast.File @@ -55,15 +93,3 @@ type AstFileInfo struct { // PackagePath package import path of the ast.File PackagePath string } - -// PackageDefinitions files and definition in a package. -type PackageDefinitions struct { - // 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 - - // package name - Name string -} diff --git a/utils.go b/utils.go new file mode 100644 index 000000000..df31ff2e1 --- /dev/null +++ b/utils.go @@ -0,0 +1,55 @@ +package swag + +import "unicode" + +// FieldsFunc split a string s by a func splitter into max n parts +func FieldsFunc(s string, f func(rune2 rune) bool, n int) []string { + // A span is used to record a slice of s of the form s[start:end]. + // The start index is inclusive and the end index is exclusive. + type span struct { + start int + end int + } + spans := make([]span, 0, 32) + + // Find the field start and end indices. + // Doing this in a separate pass (rather than slicing the string s + // and collecting the result substrings right away) is significantly + // more efficient, possibly due to cache effects. + start := -1 // valid span start if >= 0 + for end, rune := range s { + if f(rune) { + if start >= 0 { + spans = append(spans, span{start, end}) + // Set start to a negative value. + // Note: using -1 here consistently and reproducibly + // slows down this code by a several percent on amd64. + start = ^start + } + } else { + if start < 0 { + start = end + if n > 0 && len(spans)+1 >= n { + break + } + } + } + } + + // Last field might end at EOF. + if start >= 0 { + spans = append(spans, span{start, len(s)}) + } + + // Create strings from recorded field indices. + a := make([]string, len(spans)) + for i, span := range spans { + a[i] = s[span.start:span.end] + } + return a +} + +// FieldsByAnySpace split a string s by any space character into max n parts +func FieldsByAnySpace(s string, n int) []string { + return FieldsFunc(s, unicode.IsSpace, n) +} diff --git a/utils_go18.go b/utils_go18.go new file mode 100644 index 000000000..814f93433 --- /dev/null +++ b/utils_go18.go @@ -0,0 +1,31 @@ +//go:build go1.18 +// +build go1.18 + +package swag + +import ( + "reflect" + "unicode/utf8" +) + +// AppendUtf8Rune appends the UTF-8 encoding of r to the end of p and +// returns the extended buffer. If the rune is out of range, +// it appends the encoding of RuneError. +func AppendUtf8Rune(p []byte, r rune) []byte { + return utf8.AppendRune(p, r) +} + +// CanIntegerValue a wrapper of reflect.Value +type CanIntegerValue struct { + reflect.Value +} + +// CanInt reports whether Uint can be used without panicking. +func (v CanIntegerValue) CanInt() bool { + return v.Value.CanInt() +} + +// CanUint reports whether Uint can be used without panicking. +func (v CanIntegerValue) CanUint() bool { + return v.Value.CanUint() +} diff --git a/utils_other.go b/utils_other.go new file mode 100644 index 000000000..531c0df12 --- /dev/null +++ b/utils_other.go @@ -0,0 +1,47 @@ +//go:build !go1.18 +// +build !go1.18 + +package swag + +import ( + "reflect" + "unicode/utf8" +) + +// AppendUtf8Rune appends the UTF-8 encoding of r to the end of p and +// returns the extended buffer. If the rune is out of range, +// it appends the encoding of RuneError. +func AppendUtf8Rune(p []byte, r rune) []byte { + length := utf8.RuneLen(rune(r)) + if length > 0 { + utf8Slice := make([]byte, length) + utf8.EncodeRune(utf8Slice, rune(r)) + p = append(p, utf8Slice...) + } + return p +} + +// CanIntegerValue a wrapper of reflect.Value +type CanIntegerValue struct { + reflect.Value +} + +// CanInt reports whether Uint can be used without panicking. +func (v CanIntegerValue) CanInt() bool { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + default: + return false + } +} + +// CanUint reports whether Uint can be used without panicking. +func (v CanIntegerValue) CanUint() bool { + switch v.Kind() { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return true + default: + return false + } +} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 000000000..1c4d9953a --- /dev/null +++ b/utils_test.go @@ -0,0 +1,38 @@ +package swag + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestFieldsByAnySpace(t *testing.T) { + type args struct { + s string + n int + } + tests := []struct { + name string + args args + want []string + }{ + {"test1", + args{ + " aa bb cc dd ff", + 2, + }, + []string{"aa", "bb\tcc dd \t\tff"}, + }, + {"test2", + args{ + ` aa "bb cc dd ff"`, + 2, + }, + []string{"aa", `"bb cc dd ff"`}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, FieldsByAnySpace(tt.args.s, tt.args.n), "FieldsByAnySpace(%v, %v)", tt.args.s, tt.args.n) + }) + } +} diff --git a/version.go b/version.go index 0955fab62..7f24b3e24 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package swag // Version of swag. -const Version = "v1.8.1" +const Version = "v1.8.9-rc2"