Skip to content

Commit

Permalink
feat: support openapi-v3 oneOf for response
Browse files Browse the repository at this point in the history
  • Loading branch information
KKKIIO authored Aug 13, 2024
1 parent 74081a1 commit 6cc8177
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 53 deletions.
131 changes: 82 additions & 49 deletions operationv3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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))
Expand All @@ -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]])
Expand All @@ -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
Expand Down
21 changes: 19 additions & 2 deletions parserv3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -358,7 +359,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)
Expand All @@ -373,7 +373,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")
Expand Down Expand Up @@ -447,6 +447,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) {
Expand Down
14 changes: 12 additions & 2 deletions testdata/v3/simple/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

}

0 comments on commit 6cc8177

Please sign in to comment.