diff --git a/operation.go b/operation.go index 720d593..cea5051 100644 --- a/operation.go +++ b/operation.go @@ -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" @@ -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) @@ -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 { diff --git a/router_test.go b/router_test.go index 69c81b1..191be70 100644 --- a/router_test.go +++ b/router_test.go @@ -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", diff --git a/testdata/duplicate_status_code_responses_3.yaml b/testdata/duplicate_status_code_responses_3.yaml new file mode 100644 index 0000000..8ca12d2 --- /dev/null +++ b/testdata/duplicate_status_code_responses_3.yaml @@ -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 diff --git a/testdata/duplicate_status_code_responses_31.yaml b/testdata/duplicate_status_code_responses_31.yaml new file mode 100644 index 0000000..24c7670 --- /dev/null +++ b/testdata/duplicate_status_code_responses_31.yaml @@ -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