Skip to content

Commit

Permalink
feature: support default response, use comma-separated codes to add a… (
Browse files Browse the repository at this point in the history
#837)

* feature: support default response, use comma-separated codes to add a header for multi responses in one line, use keyword 'all' to add a header for all responses

* update readme_zh-CN.md

* fix tests

* fix tests coverage
  • Loading branch information
sdghchj authored Nov 20, 2020
1 parent 2ca4d38 commit 09f9621
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 84 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,9 @@ import (
// @Param id path int true "Account ID"
// @Success 200 {object} model.Account
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} httputil.HTTPError
// @Failure 404 {object} httputil.HTTPError
// @Failure 400,404 {object} httputil.HTTPError
// @Failure 500 {object} httputil.HTTPError
// @Failure default {object} httputil.DefaultError
// @Router /accounts/{id} [get]
func (c *Controller) ShowAccount(ctx *gin.Context) {
id := ctx.Param("id")
Expand All @@ -272,9 +272,9 @@ func (c *Controller) ShowAccount(ctx *gin.Context) {
// @Param q query string false "name search by q"
// @Success 200 {array} model.Account
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} httputil.HTTPError
// @Failure 404 {object} httputil.HTTPError
// @Failure 400,404 {object} httputil.HTTPError
// @Failure 500 {object} httputil.HTTPError
// @Failure default {object} httputil.DefaultError
// @Router /accounts [get]
func (c *Controller) ListAccounts(ctx *gin.Context) {
q := ctx.Request.URL.Query().Get("q")
Expand Down Expand Up @@ -375,8 +375,9 @@ When a short string in your documentation is insufficient, or you need images, c
| produce | A list of MIME types the APIs can produce. Value MUST be as described under [Mime Types](#mime-types). |
| param | Parameters that separated by spaces. `param name`,`param type`,`data type`,`is mandatory?`,`comment` `attribute(optional)` |
| security | [Security](#security) to each API operation. |
| success | Success response that separated by spaces. `return code`,`{param type}`,`data type`,`comment` |
| failure | Failure response that separated by spaces. `return code`,`{param type}`,`data type`,`comment` |
| success | Success response that separated by spaces. `return code or default`,`{param type}`,`data type`,`comment` |
| failure | Failure response that separated by spaces. `return code or default`,`{param type}`,`data type`,`comment` |
| response | As same as `success` and `failure` |
| header | Header in response that separated by spaces. `return code`,`{param type}`,`data type`,`comment` |
| router | Path definition that separated by spaces. `path`,`[httpMethod]` |
| x-name | The extension key, must be start by x- and take only json value. |
Expand Down Expand Up @@ -557,8 +558,11 @@ type DeepObject struct { //in `proto` package
```go
// @Success 200 {string} string "ok"
// @failure 400 {string} string "error"
// @response default {string} string "other error"
// @Header 200 {string} Location "/entity/1"
// @Header 200 {string} Token "qwerty"
// @Header 200,400,default {string} Token "token"
// @Header all {string} Token2 "token2"
```
### Use multiple path params
Expand Down
20 changes: 12 additions & 8 deletions README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,9 @@ import (
// @Param id path int true "Account ID"
// @Success 200 {object} model.Account
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} httputil.HTTPError
// @Failure 404 {object} httputil.HTTPError
// @Failure 400,404 {object} httputil.HTTPError
// @Failure 500 {object} httputil.HTTPError
// @Failure default {object} httputil.DefaultError
// @Router /accounts/{id} [get]
func (c *Controller) ShowAccount(ctx *gin.Context) {
id := ctx.Param("id")
Expand All @@ -268,9 +268,9 @@ func (c *Controller) ShowAccount(ctx *gin.Context) {
// @Param q query string false "name search by q"
// @Success 200 {array} model.Account
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} httputil.HTTPError
// @Failure 404 {object} httputil.HTTPError
// @Failure 400,404 {object} httputil.HTTPError
// @Failure 500 {object} httputil.HTTPError
// @Failure default {object} httputil.DefaultError
// @Router /accounts [get]
func (c *Controller) ListAccounts(ctx *gin.Context) {
q := ctx.Request.URL.Query().Get("q")
Expand Down Expand Up @@ -356,10 +356,10 @@ swag init

Example [celler/controller](https://github.com/swaggo/swag/tree/master/example/celler/controller)

| 注释 | 描述 | |
| -------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| 注释 | 描述 |
| -------------------- | ------------------------------------------------------------------------------------------------------- |
| description | 操作行为的详细说明。 |
| description.markdown | 应用程序的简短描述。该描述将从名为`endpointname.md`的文件中读取。 | // @description.file endpoint.description.markdown |
| description.markdown | 应用程序的简短描述。该描述将从名为`endpointname.md`的文件中读取。 |
| id | 用于标识操作的唯一字符串。在所有API操作中必须唯一。 |
| tags | 每个API操作的标签列表,以逗号分隔。 |
| summary | 该操作的简短摘要。 |
Expand All @@ -369,6 +369,7 @@ Example [celler/controller](https://github.com/swaggo/swag/tree/master/example/c
| security | 每个API操作的[安全性](#security)|
| success | 以空格分隔的成功响应。`return code`,`{param type}`,`data type`,`comment` |
| failure | 以空格分隔的故障响应。`return code`,`{param type}`,`data type`,`comment` |
| response | 与success、failure作用相同 |
| header | 以空格分隔的头字段。 `return code`,`{param type}`,`data type`,`comment` |
| router | 以空格分隔的路径定义。 `path`,`[httpMethod]` |
| x-name | 扩展字段必须以`x-`开头,并且只能使用json值。 |
Expand Down Expand Up @@ -536,8 +537,11 @@ type Order struct { //in `proto` package

```go
// @Success 200 {string} string "ok"
// @failure 400 {string} string "error"
// @response default {string} string "other error"
// @Header 200 {string} Location "/entity/1"
// @Header 200 {string} Token "qwerty"
// @Header 200,400,default {string} Token "token"
// @Header all {string} Token2 "token2"
```

### 使用多路径参数
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require (
github.com/ghodss/yaml v1.0.0
github.com/gin-gonic/gin v1.6.3
github.com/go-openapi/spec v0.19.14
github.com/go-openapi/swag v0.19.11 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/gofrs/uuid v3.3.0+incompatible
github.com/golang/protobuf v1.4.3 // indirect
Expand Down
158 changes: 93 additions & 65 deletions operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (operation *Operation) ParseComment(comment string, astFile *ast.File) erro
err = operation.ParseProduceComment(lineRemainder)
case "@param":
err = operation.ParseParamComment(lineRemainder, astFile)
case "@success", "@failure":
case "@success", "@failure", "@response":
err = operation.ParseResponseComment(lineRemainder, astFile)
case "@header":
err = operation.ParseResponseHeaderComment(lineRemainder, astFile)
Expand Down Expand Up @@ -611,7 +611,7 @@ func findTypeDef(importPath, typeName string) (*ast.TypeSpec, error) {
return nil, fmt.Errorf("type spec not found")
}

var responsePattern = regexp.MustCompile(`([\d]+)[\s]+([\w\{\}]+)[\s]+([\w\-\.\/\{\}=,\[\]]+)[^"]*(.*)?`)
var responsePattern = regexp.MustCompile(`([\w,]+)[\s]+([\w\{\}]+)[\s]+([\w\-\.\/\{\}=,\[\]]+)[^"]*(.*)?`)

//RepsonseType{data1=Type1,data2=Type2}
var combinedPattern = regexp.MustCompile(`^([\w\-\.\/\[\]]+)\{(.*)\}$`)
Expand Down Expand Up @@ -743,31 +743,31 @@ func (operation *Operation) ParseResponseComment(commentLine string, astFile *as
return err
}

code, _ := strconv.Atoi(matches[1])

responseDescription := strings.Trim(matches[4], "\"")
if responseDescription == "" {
responseDescription = http.StatusText(code)
}

schemaType := strings.Trim(matches[2], "{}")
refType := matches[3]
schema, err := operation.parseAPIObjectSchema(schemaType, refType, astFile)
if err != nil {
return err
}

if operation.Responses == nil {
operation.Responses = &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: make(map[int]spec.Response),
},
for _, codeStr := range strings.Split(matches[1], ",") {
if strings.EqualFold(codeStr, "default") {
operation.DefaultResponse().Schema = schema
operation.DefaultResponse().Description = responseDescription
} else if code, err := strconv.Atoi(codeStr); err == nil {
if responseDescription == "" {
responseDescription = http.StatusText(code)
}

operation.AddResponse(code, &spec.Response{
ResponseProps: spec.ResponseProps{Schema: schema, Description: responseDescription},
})
} else {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
}
}

operation.Responses.StatusCodeResponses[code] = spec.Response{
ResponseProps: spec.ResponseProps{Schema: schema, Description: responseDescription},
}
return nil
}

Expand All @@ -779,45 +779,60 @@ func (operation *Operation) ParseResponseHeaderComment(commentLine string, astFi
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
}

response := spec.Response{}

code, _ := strconv.Atoi(matches[1])

responseDescription := strings.Trim(matches[4], "\"")
if responseDescription == "" {
responseDescription = http.StatusText(code)
}
response.Description = responseDescription

schemaType := strings.Trim(matches[2], "{}")
refType := matches[3]

if operation.Responses == nil {
operation.Responses = &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: make(map[int]spec.Response),
},
headerKey := matches[3]
description := strings.Trim(matches[4], "\"")
header := spec.Header{}
header.Description = description
header.Type = schemaType

if strings.EqualFold(matches[1], "all") {
if operation.Responses.Default != nil {
if operation.Responses.Default.Headers == nil {
operation.Responses.Default.Headers = make(map[string]spec.Header)
}
operation.Responses.Default.Headers[headerKey] = header
}
if operation.Responses != nil && operation.Responses.StatusCodeResponses != nil {
for code, response := range operation.Responses.StatusCodeResponses {
if response.Headers == nil {
response.Headers = make(map[string]spec.Header)
}
response.Headers[headerKey] = header
operation.Responses.StatusCodeResponses[code] = response
}
}
return nil
}

response, responseExist := operation.Responses.StatusCodeResponses[code]
if responseExist {
header := spec.Header{}
header.Description = responseDescription
header.Type = schemaType
for _, codeStr := range strings.Split(matches[1], ",") {
if strings.EqualFold(codeStr, "default") {
if operation.Responses.Default != nil {
if operation.Responses.Default.Headers == nil {
operation.Responses.Default.Headers = make(map[string]spec.Header)
}
operation.Responses.Default.Headers[headerKey] = header
}
} else if code, err := strconv.Atoi(codeStr); err == nil {
if operation.Responses != nil && operation.Responses.StatusCodeResponses != nil {
if response, responseExist := operation.Responses.StatusCodeResponses[code]; responseExist {
if response.Headers == nil {
response.Headers = make(map[string]spec.Header)
}
response.Headers[headerKey] = header

if response.Headers == nil {
response.Headers = make(map[string]spec.Header)
operation.Responses.StatusCodeResponses[code] = response
}
}
} else {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
}
response.Headers[refType] = header

operation.Responses.StatusCodeResponses[code] = response
}

return nil
}

var emptyResponsePattern = regexp.MustCompile(`([\d]+)[\s]+"(.*)"`)
var emptyResponsePattern = regexp.MustCompile(`([\w,]+)[\s]+"(.*)"`)

// ParseEmptyResponseComment parse only comment out status code and description,eg: @Success 200 "it's ok"
func (operation *Operation) ParseEmptyResponseComment(commentLine string) error {
Expand All @@ -827,44 +842,57 @@ func (operation *Operation) ParseEmptyResponseComment(commentLine string) error
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
}

response := spec.Response{}

code, _ := strconv.Atoi(matches[1])

response.Description = strings.Trim(matches[2], "")

if operation.Responses == nil {
operation.Responses = &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: make(map[int]spec.Response),
},
responseDescription := strings.Trim(matches[2], "\"")
for _, codeStr := range strings.Split(matches[1], ",") {
if strings.EqualFold(codeStr, "default") {
operation.DefaultResponse().Description = responseDescription
} else if code, err := strconv.Atoi(codeStr); err == nil {
var response spec.Response
response.Description = responseDescription
operation.AddResponse(code, &response)
} else {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
}
}

operation.Responses.StatusCodeResponses[code] = response

return nil
}

//ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200
func (operation *Operation) ParseEmptyResponseOnly(commentLine string) error {
response := spec.Response{}
for _, codeStr := range strings.Split(commentLine, ",") {
if strings.EqualFold(codeStr, "default") {
_ = operation.DefaultResponse()
} else if code, err := strconv.Atoi(codeStr); err == nil {
var response spec.Response
//response.Description = http.StatusText(code)
operation.AddResponse(code, &response)
} else {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
}
}

code, err := strconv.Atoi(commentLine)
if err != nil {
return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
return nil
}

//DefaultResponse return the default response member pointer
func (operation *Operation) DefaultResponse() *spec.Response {
if operation.Responses.Default == nil {
operation.Responses.Default = &spec.Response{}
}
return operation.Responses.Default
}

//AddResponse add a response for a code
func (operation *Operation) AddResponse(code int, response *spec.Response) {
if operation.Responses == nil {
operation.Responses = &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: make(map[int]spec.Response),
},
}
}

operation.Responses.StatusCodeResponses[code] = response

return nil
operation.Responses.StatusCodeResponses[code] = *response
}

// createParameter returns swagger spec.Parameter for gived paramType, description, paramName, schemaType, required
Expand Down
Loading

0 comments on commit 09f9621

Please sign in to comment.