Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/oaswrap/spec/internal/debuglog"
specopenapi "github.com/oaswrap/spec/openapi"
"github.com/oaswrap/spec/option"
"github.com/swaggest/jsonschema-go"
"github.com/swaggest/openapi-go"
"github.com/swaggest/openapi-go/openapi3"
"github.com/swaggest/openapi-go/openapi31"
Expand Down Expand Up @@ -76,7 +77,7 @@ func (oc *operationContextImpl) build() openapi.OperationContext {
logger.LogOp(method, path, "add request", value)
}

for _, resp := range cfg.Responses {
for _, resp := range mergeResponses(cfg.Responses) {
opts, value := oc.buildResponseOpts(resp)
oc.op.AddRespStructure(resp.Structure, opts...)
logger.LogOp(method, path, "add response", value)
Expand All @@ -85,6 +86,60 @@ func (oc *operationContextImpl) build() openapi.OperationContext {
return oc.op
}

// responseKey identifies a unique response slot by HTTP status and content type.
type responseKey struct {
httpStatus int
contentType string
}

// mergeResponses groups responses by (HTTPStatus, ContentType) and combines
// duplicates into a single response using jsonschema.OneOf.
func mergeResponses(responses []*specopenapi.ContentUnit) []*specopenapi.ContentUnit {
if len(responses) <= 1 {
return responses
}

type group struct {
key responseKey
items []*specopenapi.ContentUnit
}

var order []responseKey
groups := make(map[responseKey]*group)

for _, resp := range responses {
k := responseKey{httpStatus: resp.HTTPStatus, contentType: resp.ContentType}
g, exists := groups[k]
if !exists {
g = &group{key: k}
groups[k] = g
order = append(order, k)
}
g.items = append(g.items, resp)
}

result := make([]*specopenapi.ContentUnit, 0, len(order))
for _, k := range order {
g := groups[k]
if len(g.items) == 1 {
result = append(result, g.items[0])
continue
}

// Merge multiple structures into oneOf.
structures := make([]interface{}, 0, len(g.items))
for _, item := range g.items {
structures = append(structures, item.Structure)
}

merged := *g.items[0] // copy first entry for description, status, etc.
merged.Structure = jsonschema.OneOf(structures...)
result = append(result, &merged)
}

return result
}

func stringMapToEncodingMap3(enc map[string]string) map[string]openapi3.Encoding {
res := map[string]openapi3.Encoding{}
for k, v := range enc {
Expand Down
19 changes: 19 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,25 @@ func TestRouter(t *testing.T) {
)
},
},
{
name: "Duplicate Status Code Responses",
golden: "duplicate_status_code_responses",
setup: func(r spec.Router) {
type SuccessA struct {
Message string `json:"message"`
}
type SuccessB struct {
Count int `json:"count"`
}
r.Get("/mixed",
option.OperationID("getMixed"),
option.Summary("Get Mixed Responses"),
option.Description("Returns one of two possible response shapes."),
option.Response(200, new(SuccessA)),
option.Response(200, new(SuccessB)),
)
},
},
{
name: "Server Variables",
golden: "server_variables",
Expand Down
32 changes: 32 additions & 0 deletions testdata/duplicate_status_code_responses_3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
openapi: 3.0.3
info:
description: This is the API documentation for Duplicate Status Code Responses
title: 'API Doc: Duplicate Status Code Responses'
version: 1.0.0
paths:
/mixed:
get:
description: Returns one of two possible response shapes.
operationId: getMixed
responses:
"200":
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/SpecTestSuccessA'
- $ref: '#/components/schemas/SpecTestSuccessB'
description: OK
summary: Get Mixed Responses
components:
schemas:
SpecTestSuccessA:
properties:
message:
type: string
type: object
SpecTestSuccessB:
properties:
count:
type: integer
type: object
32 changes: 32 additions & 0 deletions testdata/duplicate_status_code_responses_31.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
openapi: 3.1.0
info:
description: This is the API documentation for Duplicate Status Code Responses
title: 'API Doc: Duplicate Status Code Responses'
version: 1.0.0
paths:
/mixed:
get:
description: Returns one of two possible response shapes.
operationId: getMixed
responses:
"200":
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/SpecTestSuccessA'
- $ref: '#/components/schemas/SpecTestSuccessB'
description: OK
summary: Get Mixed Responses
components:
schemas:
SpecTestSuccessA:
properties:
message:
type: string
type: object
SpecTestSuccessB:
properties:
count:
type: integer
type: object