From 565bcb3f215dbcd2672eb05128a3e1ad0b613ecc Mon Sep 17 00:00:00 2001 From: Kevin Wan Date: Sun, 19 Jul 2026 23:57:26 +0800 Subject: [PATCH] feat(goctl): support custom Swagger response status (#5691) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/goctl/api/swagger/const.go | 2 + tools/goctl/api/swagger/example/example.api | 2 +- .../api/swagger/example/example.swagger.json | 2 +- .../goctl/api/swagger/example/example_cn.api | 2 +- .../swagger/example/example_cn.swagger.json | 2 +- tools/goctl/api/swagger/response.go | 126 ++++++++++++------ tools/goctl/api/swagger/response_test.go | 92 +++++++++++++ 7 files changed, 183 insertions(+), 45 deletions(-) create mode 100644 tools/goctl/api/swagger/response_test.go diff --git a/tools/goctl/api/swagger/const.go b/tools/goctl/api/swagger/const.go index 318ba5638..42dbbcccb 100644 --- a/tools/goctl/api/swagger/const.go +++ b/tools/goctl/api/swagger/const.go @@ -54,6 +54,8 @@ const ( propertyKeyDeprecated = "deprecated" propertyKeyPrefix = "prefix" propertyKeyAuthType = "authType" + propertyKeyRespCode = "respCode" + propertyKeyResponses = "responses" propertyKeyHost = "host" propertyKeyBasePath = "basePath" propertyKeyWrapCodeMsg = "wrapCodeMsg" diff --git a/tools/goctl/api/swagger/example/example.api b/tools/goctl/api/swagger/example/example.api index 6ac453483..2b8f00573 100644 --- a/tools/goctl/api/swagger/example/example.api +++ b/tools/goctl/api/swagger/example/example.api @@ -82,6 +82,7 @@ type ( service Swagger { @doc ( description: "form demo" + respCode: "201" // HTTP status code corresponding to Swagger ) @handler form post /form (FormReq) returns (FormResp) @@ -238,4 +239,3 @@ service Swagger { @handler jsonComplex post /json/complex (ComplexJsonReq) returns (ComplexJsonResp) } - diff --git a/tools/goctl/api/swagger/example/example.swagger.json b/tools/goctl/api/swagger/example/example.swagger.json index dd494fa21..a431755fc 100644 --- a/tools/goctl/api/swagger/example/example.swagger.json +++ b/tools/goctl/api/swagger/example/example.swagger.json @@ -60,7 +60,7 @@ } ], "responses": { - "200": { + "201": { "description": "", "schema": { "type": "object", diff --git a/tools/goctl/api/swagger/example/example_cn.api b/tools/goctl/api/swagger/example/example_cn.api index f48168c68..cab950b21 100644 --- a/tools/goctl/api/swagger/example/example_cn.api +++ b/tools/goctl/api/swagger/example/example_cn.api @@ -84,6 +84,7 @@ type ( service Swagger { @doc ( description: "form 接口" + respCode: "201" // 对应 Swagger 的 HTTP 状态码 ) @handler form post /form (FormReq) returns (FormResp) @@ -244,4 +245,3 @@ service Swagger { @handler jsonComplex post /json/complex (ComplexJsonReq) returns (ComplexJsonResp) } - diff --git a/tools/goctl/api/swagger/example/example_cn.swagger.json b/tools/goctl/api/swagger/example/example_cn.swagger.json index 0d949a9e0..faae89a46 100644 --- a/tools/goctl/api/swagger/example/example_cn.swagger.json +++ b/tools/goctl/api/swagger/example/example_cn.swagger.json @@ -60,7 +60,7 @@ } ], "responses": { - "200": { + "201": { "description": "", "schema": { "type": "object", diff --git a/tools/goctl/api/swagger/response.go b/tools/goctl/api/swagger/response.go index 3940a8c20..c57333b1c 100644 --- a/tools/goctl/api/swagger/response.go +++ b/tools/goctl/api/swagger/response.go @@ -2,64 +2,108 @@ package swagger import ( "net/http" + "strconv" + "strings" "github.com/go-openapi/spec" apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec" ) func jsonResponseFromType(ctx Context, atDoc apiSpec.AtDoc, tp apiSpec.Type) *spec.Responses { + statusCode := responseStatusCode(atDoc) + var response spec.Response if tp == nil { - return &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: map[int]spec.Response{ - http.StatusOK: { - ResponseProps: spec.ResponseProps{ - Description: "", - Schema: &spec.Schema{}, - }, - }, - }, + response = spec.Response{ + ResponseProps: spec.ResponseProps{ + Description: "", }, } - } - props := spec.SchemaProps{ - AdditionalProperties: mapFromGoType(ctx, tp), - Items: itemsFromGoType(ctx, tp), - } - if ctx.UseDefinitions { - structName, ok := containsStruct(tp) - if ok { - props.Ref = spec.MustCreateRef(getRefName(structName)) - return &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: map[int]spec.Response{ - http.StatusOK: { - ResponseProps: spec.ResponseProps{ - Schema: &spec.Schema{ - SchemaProps: wrapCodeMsgProps(ctx, props, atDoc), - }, - }, - }, - }, - }, - } + } else { + props := spec.SchemaProps{ + AdditionalProperties: mapFromGoType(ctx, tp), + Items: itemsFromGoType(ctx, tp), } - } - - p, _ := propertiesFromType(ctx, tp) - props.Type = typeFromGoType(ctx, tp) - props.Properties = p - return &spec.Responses{ - ResponsesProps: spec.ResponsesProps{ - StatusCodeResponses: map[int]spec.Response{ - http.StatusOK: { + if ctx.UseDefinitions { + structName, ok := containsStruct(tp) + if ok { + props.Ref = spec.MustCreateRef(getRefName(structName)) + response = spec.Response{ ResponseProps: spec.ResponseProps{ Schema: &spec.Schema{ SchemaProps: wrapCodeMsgProps(ctx, props, atDoc), }, }, + } + return responsesFromStatusCode(atDoc, statusCode, response) + } + } + + p, _ := propertiesFromType(ctx, tp) + props.Type = typeFromGoType(ctx, tp) + props.Properties = p + response = spec.Response{ + ResponseProps: spec.ResponseProps{ + Schema: &spec.Schema{ + SchemaProps: wrapCodeMsgProps(ctx, props, atDoc), }, }, + } + } + + return responsesFromStatusCode(atDoc, statusCode, response) +} + +func responsesFromStatusCode(atDoc apiSpec.AtDoc, statusCode int, response spec.Response) *spec.Responses { + statusCodeResponses := map[int]spec.Response{ + statusCode: response, + } + for code, description := range responseDescriptions(atDoc) { + if code == statusCode { + responseWithDescription := response + responseWithDescription.Description = description + statusCodeResponses[code] = responseWithDescription + } else { + statusCodeResponses[code] = spec.Response{ + ResponseProps: spec.ResponseProps{ + Description: description, + }, + } + } + } + + return &spec.Responses{ + ResponsesProps: spec.ResponsesProps{ + StatusCodeResponses: statusCodeResponses, }, } } + +func responseStatusCode(atDoc apiSpec.AtDoc) int { + return getOrDefault(atDoc.Properties, propertyKeyRespCode, http.StatusOK, func(str string, def int) int { + statusCode, err := strconv.Atoi(str) + if err != nil || statusCode < http.StatusContinue || statusCode > 599 { + return def + } + + return statusCode + }) +} + +func responseDescriptions(atDoc apiSpec.AtDoc) map[int]string { + descriptions := make(map[int]string) + for _, item := range strings.Split(getStringFromKVOrDefault(atDoc.Properties, propertyKeyResponses, ""), "
") { + codeText, description, ok := strings.Cut(item, "-") + if !ok { + continue + } + + code, err := strconv.Atoi(strings.TrimSpace(codeText)) + if err != nil || code < http.StatusContinue || code > 599 { + continue + } + + descriptions[code] = strings.TrimSpace(description) + } + + return descriptions +} diff --git a/tools/goctl/api/swagger/response_test.go b/tools/goctl/api/swagger/response_test.go new file mode 100644 index 000000000..d95f4422d --- /dev/null +++ b/tools/goctl/api/swagger/response_test.go @@ -0,0 +1,92 @@ +package swagger + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/tools/goctl/api/spec" +) + +func TestJsonResponseFromTypeStatusCode(t *testing.T) { + tests := []struct { + name string + properties map[string]string + response spec.Type + want int + }{ + { + name: "defaults to ok", + response: spec.PrimitiveType{RawName: "string"}, + want: http.StatusOK, + }, + { + name: "uses custom status code", + properties: map[string]string{ + propertyKeyRespCode: "201", + }, + response: spec.PrimitiveType{RawName: "string"}, + want: http.StatusCreated, + }, + { + name: "supports quoted custom status code", + properties: map[string]string{ + propertyKeyRespCode: `"204"`, + }, + want: http.StatusNoContent, + }, + { + name: "defaults for invalid status code", + properties: map[string]string{ + propertyKeyRespCode: "600", + }, + response: spec.PrimitiveType{RawName: "string"}, + want: http.StatusOK, + }, + { + name: "defaults for non-numeric status code", + properties: map[string]string{ + propertyKeyRespCode: "created", + }, + response: spec.PrimitiveType{RawName: "string"}, + want: http.StatusOK, + }, + { + name: "uses custom status code without response body", + properties: map[string]string{ + propertyKeyRespCode: "204", + }, + want: http.StatusNoContent, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + responses := jsonResponseFromType(testingContext(t), spec.AtDoc{ + Properties: test.properties, + }, test.response) + + assert.Len(t, responses.StatusCodeResponses, 1) + assert.Contains(t, responses.StatusCodeResponses, test.want) + if test.response == nil { + assert.Nil(t, responses.StatusCodeResponses[test.want].Schema) + } + }) + } +} + +func TestJsonResponseFromTypeMultipleStatusCodes(t *testing.T) { + responses := jsonResponseFromType(testingContext(t), spec.AtDoc{ + Properties: map[string]string{ + propertyKeyResponses: "200-OK
401-Unauthorized
404-User not found", + }, + }, spec.PrimitiveType{RawName: "string"}) + + assert.Len(t, responses.StatusCodeResponses, 3) + assert.Equal(t, "OK", responses.StatusCodeResponses[http.StatusOK].Description) + assert.NotNil(t, responses.StatusCodeResponses[http.StatusOK].Schema) + assert.Equal(t, "Unauthorized", responses.StatusCodeResponses[http.StatusUnauthorized].Description) + assert.Nil(t, responses.StatusCodeResponses[http.StatusUnauthorized].Schema) + assert.Equal(t, "User not found", responses.StatusCodeResponses[http.StatusNotFound].Description) + assert.Nil(t, responses.StatusCodeResponses[http.StatusNotFound].Schema) +}