From 09f9621f095995bdf1a6540f4676103585579fe8 Mon Sep 17 00:00:00 2001 From: sdghchj Date: Fri, 20 Nov 2020 17:42:18 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20support=20default=20response,=20use?= =?UTF-8?q?=20comma-separated=20codes=20to=20add=20a=E2=80=A6=20(#837)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- README.md | 18 ++++-- README_zh-CN.md | 20 +++--- go.mod | 1 - operation.go | 158 ++++++++++++++++++++++++++------------------- operation_test.go | 159 +++++++++++++++++++++++++++++++++++++++++++++- parser.go | 2 +- 6 files changed, 274 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index cd2466ac2..1ddc6a78c 100644 --- a/README.md +++ b/README.md @@ -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") @@ -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") @@ -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. | @@ -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 diff --git a/README_zh-CN.md b/README_zh-CN.md index 4c3a20220..fc8edac0e 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -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") @@ -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") @@ -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 | 该操作的简短摘要。 | @@ -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值。 | @@ -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" ``` ### 使用多路径参数 diff --git a/go.mod b/go.mod index 8d12b65d0..ec3f9d38f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/operation.go b/operation.go index be943a7e4..096be6dcf 100644 --- a/operation.go +++ b/operation.go @@ -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) @@ -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\-\.\/\[\]]+)\{(.*)\}$`) @@ -743,13 +743,7 @@ 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) @@ -757,17 +751,23 @@ func (operation *Operation) ParseResponseComment(commentLine string, astFile *as 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 } @@ -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 { @@ -827,33 +842,49 @@ func (operation *Operation) ParseEmptyResponseComment(commentLine string) error return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } - response := spec.Response{} - - code, _ := strconv.Atoi(matches[1]) - - response.Description = strings.Trim(matches[2], "") - - if operation.Responses == nil { - operation.Responses = &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: make(map[int]spec.Response), - }, + responseDescription := strings.Trim(matches[2], "\"") + for _, codeStr := range strings.Split(matches[1], ",") { + if strings.EqualFold(codeStr, "default") { + operation.DefaultResponse().Description = responseDescription + } else if code, err := strconv.Atoi(codeStr); err == nil { + var response spec.Response + response.Description = responseDescription + operation.AddResponse(code, &response) + } else { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } } - operation.Responses.StatusCodeResponses[code] = response - return nil } //ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200 func (operation *Operation) ParseEmptyResponseOnly(commentLine string) error { - response := spec.Response{} + for _, codeStr := range strings.Split(commentLine, ",") { + if strings.EqualFold(codeStr, "default") { + _ = operation.DefaultResponse() + } else if code, err := strconv.Atoi(codeStr); err == nil { + var response spec.Response + //response.Description = http.StatusText(code) + operation.AddResponse(code, &response) + } else { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + } - code, err := strconv.Atoi(commentLine) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + return nil +} + +//DefaultResponse return the default response member pointer +func (operation *Operation) DefaultResponse() *spec.Response { + if operation.Responses.Default == nil { + operation.Responses.Default = &spec.Response{} } + return operation.Responses.Default +} + +//AddResponse add a response for a code +func (operation *Operation) AddResponse(code int, response *spec.Response) { if operation.Responses == nil { operation.Responses = &spec.Responses{ ResponsesProps: spec.ResponsesProps{ @@ -861,10 +892,7 @@ func (operation *Operation) ParseEmptyResponseOnly(commentLine string) error { }, } } - - operation.Responses.StatusCodeResponses[code] = response - - return nil + operation.Responses.StatusCodeResponses[code] = *response } // createParameter returns swagger spec.Parameter for gived paramType, description, paramName, schemaType, required diff --git a/operation_test.go b/operation_test.go index fb2403eee..be82dc3f2 100644 --- a/operation_test.go +++ b/operation_test.go @@ -671,6 +671,38 @@ func TestParseResponseCommentWithBasicType(t *testing.T) { assert.Equal(t, expected, string(b)) } +func TestParseResponseCommentWithBasicTypeAndCodes(t *testing.T) { + comment := `@Success 200,201,default {string} string "it's ok'"` + operation := NewOperation(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "it's ok'", + "schema": { + "type": "string" + } + }, + "201": { + "description": "it's ok'", + "schema": { + "type": "string" + } + }, + "default": { + "description": "it's ok'", + "schema": { + "type": "string" + } + } + } +}` + assert.Equal(t, expected, string(b)) +} + func TestParseEmptyResponseComment(t *testing.T) { comment := `@Success 200 "it's ok"` operation := NewOperation(nil) @@ -689,6 +721,30 @@ func TestParseEmptyResponseComment(t *testing.T) { assert.Equal(t, expected, string(b)) } +func TestParseEmptyResponseCommentWithCodes(t *testing.T) { + comment := `@Success 200,201,default "it's ok"` + operation := NewOperation(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "it's ok" + }, + "201": { + "description": "it's ok" + }, + "default": { + "description": "it's ok" + } + } +}` + assert.Equal(t, expected, string(b)) +} + func TestParseResponseCommentWithHeader(t *testing.T) { comment := `@Success 200 "it's ok"` operation := NewOperation(nil) @@ -720,7 +776,74 @@ func TestParseResponseCommentWithHeader(t *testing.T) { comment = `@Header 200 "Mallformed"` err = operation.ParseComment(comment, nil) assert.Error(t, err, "ParseComment should not fail") +} + +func TestParseResponseCommentWithHeaderForCodes(t *testing.T) { + operation := NewOperation(nil) + + comment := `@Success 200,201,default "it's ok"` + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + comment = `@Header 200,201,default {string} Token "qwerty"` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + comment = `@Header all {string} Token2 "qwerty"` + err = operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + b, err := json.MarshalIndent(operation, "", " ") + assert.NoError(t, err) + expected := `{ + "responses": { + "200": { + "description": "it's ok", + "headers": { + "Token": { + "type": "string", + "description": "qwerty" + }, + "Token2": { + "type": "string", + "description": "qwerty" + } + } + }, + "201": { + "description": "it's ok", + "headers": { + "Token": { + "type": "string", + "description": "qwerty" + }, + "Token2": { + "type": "string", + "description": "qwerty" + } + } + }, + "default": { + "description": "it's ok", + "headers": { + "Token": { + "type": "string", + "description": "qwerty" + }, + "Token2": { + "type": "string", + "description": "qwerty" + } + } + } + } +}` + assert.Equal(t, expected, string(b)) + + comment = `@Header 200 "Mallformed"` + err = operation.ParseComment(comment, nil) + assert.Error(t, err, "ParseComment should not fail") } func TestParseEmptyResponseOnlyCode(t *testing.T) { @@ -741,12 +864,44 @@ func TestParseEmptyResponseOnlyCode(t *testing.T) { assert.Equal(t, expected, string(b)) } +func TestParseEmptyResponseOnlyCodes(t *testing.T) { + comment := `@Success 200,201,default` + operation := NewOperation(nil) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err, "ParseComment should not fail") + + b, _ := json.MarshalIndent(operation, "", " ") + + expected := `{ + "responses": { + "200": { + "description": "" + }, + "201": { + "description": "" + }, + "default": { + "description": "" + } + } +}` + assert.Equal(t, expected, string(b)) +} + func TestParseResponseCommentParamMissing(t *testing.T) { operation := NewOperation(nil) - paramLenErrComment := `@Success notIntCode {string}` + paramLenErrComment := `@Success notIntCode` paramLenErr := operation.ParseComment(paramLenErrComment, nil) - assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode {string}"`) + assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode"`) + + paramLenErrComment = `@Success notIntCode {string} string "it ok"` + paramLenErr = operation.ParseComment(paramLenErrComment, nil) + assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode {string} string "it ok""`) + + paramLenErrComment = `@Success notIntCode "it ok"` + paramLenErr = operation.ParseComment(paramLenErrComment, nil) + assert.EqualError(t, paramLenErr, `can not parse response comment "notIntCode "it ok""`) } // Test ParseParamComment diff --git a/parser.go b/parser.go index 0a475517a..5b2872349 100644 --- a/parser.go +++ b/parser.go @@ -410,7 +410,7 @@ func isGeneralAPIComment(comment *ast.CommentGroup) bool { attribute := strings.ToLower(strings.Split(commentLine, " ")[0]) switch attribute { // The @summary, @router, @success,@failure annotation belongs to Operation - case "@summary", "@router", "@success", "@failure": + case "@summary", "@router", "@success", "@failure", "@response": return false } }