From 2b10a6a223ec3071030abbf6c46f878a15d01c29 Mon Sep 17 00:00:00 2001 From: Kevin Wan Date: Sun, 12 Oct 2025 18:24:11 +0800 Subject: [PATCH] fix: support PUT, PATCH, DELETE methods for request body definitions in swagger (#5239) --- tools/goctl/api/swagger/parameter.go | 33 +++++++++++++++-------- tools/goctl/api/swagger/parameter_test.go | 17 +++++++++--- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/tools/goctl/api/swagger/parameter.go b/tools/goctl/api/swagger/parameter.go index 1683161ac..0530bbcf7 100644 --- a/tools/goctl/api/swagger/parameter.go +++ b/tools/goctl/api/swagger/parameter.go @@ -8,28 +8,37 @@ import ( apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec" ) -func isPostJson(ctx Context, method string, tp apiSpec.Type) (string, bool) { - if !strings.EqualFold(method, http.MethodPost) { +func isRequestBodyJson(ctx Context, method string, tp apiSpec.Type) (string, bool) { + // Support HTTP methods that commonly use request bodies with JSON + // POST, PUT, PATCH are standard methods with bodies + // DELETE can also have a body (though less common) + method = strings.ToUpper(method) + if method != http.MethodPost && method != http.MethodPut && + method != http.MethodPatch && method != http.MethodDelete { return "", false } + structType, ok := tp.(apiSpec.DefineStruct) if !ok { return "", false } - var isPostJson bool + + var hasJsonField bool rangeMemberAndDo(ctx, structType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) { jsonTag, _ := tag.Get(tagJson) - if !isPostJson { - isPostJson = jsonTag != nil + if !hasJsonField { + hasJsonField = jsonTag != nil } }) - return structType.RawName, isPostJson + + return structType.RawName, hasJsonField } func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Parameter { if tp == nil { return []spec.Parameter{} } + structType, ok := tp.(apiSpec.DefineStruct) if !ok { return []spec.Parameter{} @@ -43,15 +52,13 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para rangeMemberAndDo(ctx, structType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) { headerTag, _ := tag.Get(tagHeader) hasHeader := headerTag != nil - pathParameterTag, _ := tag.Get(tagPath) hasPathParameter := pathParameterTag != nil - formTag, _ := tag.Get(tagForm) hasForm := formTag != nil - jsonTag, _ := tag.Get(tagJson) hasJson := jsonTag != nil + if hasHeader { minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(headerTag.Options) resp = append(resp, spec.Parameter{ @@ -75,6 +82,7 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para }, }) } + if hasPathParameter { minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(pathParameterTag.Options) resp = append(resp, spec.Parameter{ @@ -98,6 +106,7 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para }, }) } + if hasForm { minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(formTag.Options) if strings.EqualFold(method, http.MethodGet) { @@ -145,8 +154,8 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para }, }) } - } + if hasJson { minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(jsonTag.Options) if required { @@ -179,9 +188,10 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para properties[jsonTag.Name] = schema } }) + if len(properties) > 0 { if ctx.UseDefinitions { - structName, ok := isPostJson(ctx, method, tp) + structName, ok := isRequestBodyJson(ctx, method, tp) if ok { resp = append(resp, spec.Parameter{ ParamProps: spec.ParamProps{ @@ -213,5 +223,6 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para }) } } + return resp } diff --git a/tools/goctl/api/swagger/parameter_test.go b/tools/goctl/api/swagger/parameter_test.go index 6c8785508..92939dd33 100644 --- a/tools/goctl/api/swagger/parameter_test.go +++ b/tools/goctl/api/swagger/parameter_test.go @@ -8,7 +8,7 @@ import ( apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec" ) -func TestIsPostJson(t *testing.T) { +func TestIsRequestBodyJson(t *testing.T) { tests := []struct { name string method string @@ -18,13 +18,18 @@ func TestIsPostJson(t *testing.T) { {"POST with JSON", http.MethodPost, true, true}, {"POST without JSON", http.MethodPost, false, false}, {"GET with JSON", http.MethodGet, true, false}, - {"PUT with JSON", http.MethodPut, true, false}, + {"PUT with JSON", http.MethodPut, true, true}, + {"PUT without JSON", http.MethodPut, false, false}, + {"PATCH with JSON", http.MethodPatch, true, true}, + {"PATCH without JSON", http.MethodPatch, false, false}, + {"DELETE with JSON", http.MethodDelete, true, true}, + {"DELETE without JSON", http.MethodDelete, false, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { testStruct := createTestStruct("TestStruct", tt.hasJson) - _, result := isPostJson(testingContext(t), tt.method, testStruct) + _, result := isRequestBodyJson(testingContext(t), tt.method, testStruct) assert.Equal(t, tt.expected, result) }) } @@ -41,6 +46,12 @@ func TestParametersFromType(t *testing.T) { }{ {"POST JSON with definitions", http.MethodPost, true, true, 1, true}, {"POST JSON without definitions", http.MethodPost, false, true, 1, true}, + {"PUT JSON with definitions", http.MethodPut, true, true, 1, true}, + {"PUT JSON without definitions", http.MethodPut, false, true, 1, true}, + {"PATCH JSON with definitions", http.MethodPatch, true, true, 1, true}, + {"PATCH JSON without definitions", http.MethodPatch, false, true, 1, true}, + {"DELETE JSON with definitions", http.MethodDelete, true, true, 1, true}, + {"DELETE JSON without definitions", http.MethodDelete, false, true, 1, true}, {"GET with form", http.MethodGet, false, false, 1, false}, {"POST with form", http.MethodPost, false, false, 1, false}, }