From 4d1de4bb6f4f3dff4272f8c3df5fa0cd76ba58a8 Mon Sep 17 00:00:00 2001 From: Patrick McLaughlin Date: Tue, 28 Jan 2025 16:52:36 -0500 Subject: [PATCH] fix: deduplicate header parameters --- packages/openapi-generator/src/route.ts | 39 ++++++------ .../test/openapi/union.test.ts | 61 +++++++++++++++++++ 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/packages/openapi-generator/src/route.ts b/packages/openapi-generator/src/route.ts index 082d671c..fa7c84bb 100644 --- a/packages/openapi-generator/src/route.ts +++ b/packages/openapi-generator/src/route.ts @@ -157,25 +157,30 @@ function parseRequestUnion( }); } if (headerSchema.schemas.length > 0) { - // For headers in unions, take properties from first schema that has headers - // Also not perfect but we cannot use the `explode: true` trick for headers - const firstHeaderSchema = schema.schemas.find( - (s) => s.type === 'object' && s.properties['headers']?.type === 'object', - ); - if ( - firstHeaderSchema?.type === 'object' && - firstHeaderSchema.properties['headers']?.type === 'object' - ) { - const headers = firstHeaderSchema.properties['headers']; - for (const [name, prop] of Object.entries(headers.properties)) { - parameters.push({ - type: 'header', - name, - schema: prop, - required: headers.required.includes(name), - }); + // For headers in unions, deduplicate and merge properties from all schemas + const headerParams = new Map(); + + for (const subSchema of schema.schemas) { + if ( + subSchema.type === 'object' && + subSchema.properties['headers']?.type === 'object' + ) { + const headers = subSchema.properties['headers']; + for (const [name, prop] of Object.entries(headers.properties)) { + // Only add if not already present + if (!headerParams.has(name)) { + headerParams.set(name, { + type: 'header', + name, + schema: prop, + required: headers.required.includes(name), + }); + } + } } } + + parameters.push(...headerParams.values()); } const firstSubSchema = schema.schemas[0]; diff --git a/packages/openapi-generator/test/openapi/union.test.ts b/packages/openapi-generator/test/openapi/union.test.ts index 443f1b5d..ab03df25 100644 --- a/packages/openapi-generator/test/openapi/union.test.ts +++ b/packages/openapi-generator/test/openapi/union.test.ts @@ -375,6 +375,67 @@ testCase("route with unknown unions", ROUTE_WITH_UNKNOWN_UNIONS, { }, }); +const ROUTE_WITH_DUPLICATE_HEADERS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: t.union([ + h.httpRequest({ + headers: { + 'x-foo': t.string, + 'x-common': t.string, + }, + }), + h.httpRequest({ + headers: { + 'x-bar': t.number, + 'x-common': t.string, + }, + }), + ]), + response: { + 200: t.string, + }, +}); +`; + +testCase("route with duplicate headers in request union", ROUTE_WITH_DUPLICATE_HEADERS, { + info: { + title: 'Test', + version: '1.0.0' + }, + openapi: '3.0.3', + paths: { + '/foo': { + get: { + parameters: [ + { in: 'header', name: 'x-foo', required: true, schema: { type: 'string' } }, + { in: 'header', name: 'x-common', required: true, schema: { type: 'string' } }, + { in: 'header', name: 'x-bar', required: true, schema: { type: 'number' } }, + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'string' + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + const ROUTE_WITH_REQUEST_UNION = ` import * as t from 'io-ts'; import * as h from '@api-ts/io-ts-http';