From bb1fa92fda0243594caccfbb31dcedd242e7e678 Mon Sep 17 00:00:00 2001 From: KKKIIO Date: Tue, 13 Aug 2024 17:05:24 +0000 Subject: [PATCH] feat: support openapi-v3 oneOf for response --- operationv3.go | 131 +++++++++++++++++++++------------- parserv3_test.go | 21 +++++- testdata/v3/simple/api/api.go | 14 +++- 3 files changed, 113 insertions(+), 53 deletions(-) diff --git a/operationv3.go b/operationv3.go index 4737694e1..5d94f1045 100644 --- a/operationv3.go +++ b/operationv3.go @@ -926,22 +926,15 @@ func (o *OperationV3) ParseResponseComment(commentLine string, astFile *ast.File for _, codeStr := range strings.Split(matches[1], ",") { if strings.EqualFold(codeStr, defaultTag) { - response := o.DefaultResponse() - response.Description = description - - mimeType := "application/json" // TODO: set correct mimeType - setResponseSchema(response, mimeType, schema) - - continue - } - - code, err := strconv.Atoi(codeStr) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) - } - - if description == "" { - description = http.StatusText(code) + codeStr = "" + } else { + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + if description == "" { + description = http.StatusText(code) + } } response := spec.NewResponseSpec() @@ -979,15 +972,12 @@ func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error { for _, codeStr := range strings.Split(matches[1], ",") { if strings.EqualFold(codeStr, defaultTag) { - response := o.DefaultResponse() - response.Description = description - - continue - } - - _, err := strconv.Atoi(codeStr) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + codeStr = "" + } else { + _, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } } o.AddResponse(codeStr, newResponseWithDescription(description)) @@ -996,21 +986,10 @@ func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error { return nil } -// DefaultResponse return the default response member pointer. -func (o *OperationV3) DefaultResponse() *spec.Response { - if o.Responses.Spec.Default == nil { - o.Responses.Spec.Default = spec.NewResponseSpec() - o.Responses.Spec.Default.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) - } - - if o.Responses.Spec.Default.Spec.Spec.Content == nil { - o.Responses.Spec.Default.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) - } - - return o.Responses.Spec.Default.Spec.Spec -} - // AddResponse add a response for a code. +// If the code is already exist, it will merge with the old one: +// 1. The description will be replaced by the new one if the new one is not empty. +// 2. The content schema will be merged using `oneOf` if the new one is not empty. func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Extendable[spec.Response]]) { if response.Spec.Spec.Headers == nil { response.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) @@ -1020,24 +999,78 @@ func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Ext o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]]) } - o.Responses.Spec.Response[code] = response + res := response + var prev *spec.RefOrSpec[spec.Extendable[spec.Response]] + if code != "" { + prev = o.Responses.Spec.Response[code] + } else { + prev = o.Responses.Spec.Default + } + if prev != nil { // merge into prev + res = prev + if response.Spec.Spec.Description != "" { + prev.Spec.Spec.Description = response.Spec.Spec.Description + } + if len(response.Spec.Spec.Content) > 0 { + // responses should only have one content type + singleKey := "" + for k := range response.Spec.Spec.Content { + singleKey = k + break + } + if prevMediaType := prev.Spec.Spec.Content[singleKey]; prevMediaType == nil { + prev.Spec.Spec.Content = response.Spec.Spec.Content + } else { + newMediaType := response.Spec.Spec.Content[singleKey] + if len(newMediaType.Extensions) > 0 { + if prevMediaType.Extensions == nil { + prevMediaType.Extensions = make(map[string]interface{}) + } + for k, v := range newMediaType.Extensions { + prevMediaType.Extensions[k] = v + } + } + if len(newMediaType.Spec.Examples) > 0 { + if prevMediaType.Spec.Examples == nil { + prevMediaType.Spec.Examples = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Example]]) + } + for k, v := range newMediaType.Spec.Examples { + prevMediaType.Spec.Examples[k] = v + } + } + if prevSchema := prevMediaType.Spec.Schema; prevSchema.Ref != nil || prevSchema.Spec.OneOf == nil { + oneOfSchema := spec.NewSchemaSpec() + oneOfSchema.Spec.OneOf = []*spec.RefOrSpec[spec.Schema]{prevSchema, newMediaType.Spec.Schema} + prevMediaType.Spec.Schema = oneOfSchema + } else { + prevSchema.Spec.OneOf = append(prevSchema.Spec.OneOf, newMediaType.Spec.Schema) + } + } + } + } + + if code != "" { + o.Responses.Spec.Response[code] = res + } else { + o.Responses.Spec.Default = res + } } // ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200. func (o *OperationV3) ParseEmptyResponseOnly(commentLine string) error { for _, codeStr := range strings.Split(commentLine, ",") { + var description string if strings.EqualFold(codeStr, defaultTag) { - _ = o.DefaultResponse() - - continue - } - - code, err := strconv.Atoi(codeStr) - if err != nil { - return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + codeStr = "" + } else { + code, err := strconv.Atoi(codeStr) + if err != nil { + return fmt.Errorf("can not parse response comment \"%s\"", commentLine) + } + description = http.StatusText(code) } - o.AddResponse(codeStr, newResponseWithDescription(http.StatusText(code))) + o.AddResponse(codeStr, newResponseWithDescription(description)) } return nil diff --git a/parserv3_test.go b/parserv3_test.go index a39f5075d..c1b561eba 100644 --- a/parserv3_test.go +++ b/parserv3_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/sv-tools/openapi/spec" ) func TestOverridesGetTypeSchemaV3(t *testing.T) { @@ -362,7 +363,6 @@ func TestParseSimpleApiV3(t *testing.T) { assert.NoError(t, err) paths := p.openAPI.Paths.Spec.Paths - assert.Equal(t, 16, len(paths)) path := paths["/testapi/get-string-by-int/{some_id}"].Spec.Spec.Get.Spec assert.Equal(t, "get string by ID", path.Description) @@ -377,7 +377,7 @@ func TestParseSimpleApiV3(t *testing.T) { assert.NotNil(t, path.RequestBody) //TODO add asserts - t.Run("Test parse oneOf", func(t *testing.T) { + t.Run("Test parse struct oneOf", func(t *testing.T) { t.Parallel() assert.Contains(t, p.openAPI.Components.Spec.Schemas, "web.OneOfTest") @@ -451,6 +451,23 @@ func TestParseSimpleApiV3(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expected, string(out)) }) + + t.Run("Test parse response oneOf", func(t *testing.T) { + t.Parallel() + + assert.Contains(t, paths, "/pets/{id}") + path := paths["/pets/{id}"] + assert.Contains(t, path.Spec.Spec.Get.Spec.Responses.Spec.Response, "200") + response = path.Spec.Spec.Get.Spec.Responses.Spec.Response["200"] + assert.Equal(t, "Return Cat or Dog", response.Spec.Spec.Description) + mediaType := response.Spec.Spec.Content["application/json"] + rootSchema := mediaType.Spec.Schema.Spec + assert.Equal(t, []*spec.RefOrSpec[spec.Schema]{ + {Ref: &spec.Ref{Ref: "#/components/schemas/web.Cat"}}, + {Ref: &spec.Ref{Ref: "#/components/schemas/web.Dog"}}, + }, rootSchema.OneOf) + + }) } func TestParserParseServers(t *testing.T) { diff --git a/testdata/v3/simple/api/api.go b/testdata/v3/simple/api/api.go index 0f6c9d07d..39ca8bad6 100644 --- a/testdata/v3/simple/api/api.go +++ b/testdata/v3/simple/api/api.go @@ -146,7 +146,17 @@ func FormData() { } // @Success 200 {object} web.OneOfTest -// @Router /OneOf [get] -func GetOneOf() { +// @Router /GetOneOfTypes [get] +func GetOneOfTypes() { + +} + +// @Summary Get pet by ID +// @Param id path string true "ID" +// @Success 200 {object} web.Cat +// @Success 200 {object} web.Dog +// @Success 200 "Return Cat or Dog" +// @Router /pets/{id} [get] +func GetPetByID() { }