diff --git a/packages/openapi-generator/src/openapi.ts b/packages/openapi-generator/src/openapi.ts index a7fe83de..15d39626 100644 --- a/packages/openapi-generator/src/openapi.ts +++ b/packages/openapi-generator/src/openapi.ts @@ -54,16 +54,33 @@ export function schemaToOpenAPI( const { example, minItems, maxItems, ...rest } = defaultOpenAPIObject; const isArrayExample = example && Array.isArray(example); + const siblings = { + ...rest, + ...(!isArrayExample && example ? { example } : {}), + }; + + // Handle case where innerSchema is a $ref with siblings + const wrappedInnerSchema = + '$ref' in innerSchema && Object.keys(siblings).length > 0 + ? // When there's a $ref with siblings, we need to wrap it in allOf to preserve other properties + { + allOf: [innerSchema], + } + : { + ...innerSchema, + }; + + const items = { + ...wrappedInnerSchema, + ...siblings, + }; + return { type: 'array', ...(minItems ? { minItems } : {}), ...(maxItems ? { maxItems } : {}), ...(isArrayExample ? { example } : {}), - items: { - ...innerSchema, - ...rest, - ...(!isArrayExample && example ? { example } : {}), - }, + items, }; case 'object': return { @@ -153,23 +170,25 @@ export function schemaToOpenAPI( if (oneOf.length === 0) { return undefined; } else if (oneOf.length === 1) { - if ( - Object.keys( - oneOf[0] as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, - )[0] === '$ref' - ) + const singleSchema = oneOf[0]; + if (singleSchema === undefined) { + return undefined; + } + // Check if the schema is a $ref + if ('$ref' in singleSchema) { // OpenAPI spec doesn't allow $ref properties to have siblings, so they're wrapped in an 'allOf' array return { ...(nullable ? { nullable } : {}), - allOf: oneOf, + allOf: [singleSchema], ...defaultOpenAPIObject, }; - else + } else { return { ...(nullable ? { nullable } : {}), - ...oneOf[0], + ...singleSchema, ...defaultOpenAPIObject, }; + } } else { return { ...(nullable ? { nullable } : {}), oneOf, ...defaultOpenAPIObject }; } diff --git a/packages/openapi-generator/test/openapi/ref.test.ts b/packages/openapi-generator/test/openapi/ref.test.ts index c52d5369..8673db01 100644 --- a/packages/openapi-generator/test/openapi/ref.test.ts +++ b/packages/openapi-generator/test/openapi/ref.test.ts @@ -336,7 +336,7 @@ const SimpleRouteResponse = t.type({ * @title Human Readable Invalid Error Schema */ const InvalidError = t.intersection([ - ApiError, + ApiError, t.type({ error: t.literal('invalid') })]); /** * Human readable description of the ApiError schema @@ -441,3 +441,201 @@ testCase('route with api error schema', ROUTE_WITH_SCHEMA_WITH_COMMENT, { }, }, }); + +const ROUTE_WITH_ARRAY_OF_INNER_SCHEMA_REF_WITH_NO_SIBLINGS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; +/** + * A simple route with type descriptions for references + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: SimpleRouteResponse + }, + }); + + + /** + * Human readable description of the Simple Route Response + * @title Human Readable Simple Route Response + */ +const SimpleRouteResponse = t.type({ + test: t.array(TestRef) +}); + +const TestRefBase = t.string; + +const TestRef = TestRefBase; + `; + +testCase( + 'inner schema ref in array', + ROUTE_WITH_ARRAY_OF_INNER_SCHEMA_REF_WITH_NO_SIBLINGS, + { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions for references', + operationId: 'api.v1.test', + tags: ['Test Routes'], + parameters: [], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/SimpleRouteResponse', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + SimpleRouteResponse: { + description: 'Human readable description of the Simple Route Response', + properties: { + test: { + type: 'array', + items: { + $ref: '#/components/schemas/TestRefBase', + }, + }, + }, + required: ['test'], + title: 'Human Readable Simple Route Response', + type: 'object', + }, + TestRefBase: { + title: 'TestRefBase', + type: 'string', + }, + TestRef: { + allOf: [ + { + title: 'TestRef', + }, + { + $ref: '#/components/schemas/TestRefBase', + }, + ], + }, + }, + }, + }, +); + +const ROUTE_WITH_ARRAY_OF_INNER_SCHEMA_REF_AND_DESCRIPTION = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; +/** + * A simple route with type descriptions for references + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: SimpleRouteResponse + }, + }); + + + /** + * Human readable description of the Simple Route Response + * @title Human Readable Simple Route Response + */ +const SimpleRouteResponse = t.type({ + /** List of test refs */ + test: t.array(TestRef) +}); + +const TestRefBase = t.string; + +const TestRef = TestRefBase; + `; + +testCase( + 'inner schema ref in array with description', + ROUTE_WITH_ARRAY_OF_INNER_SCHEMA_REF_AND_DESCRIPTION, + { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions for references', + operationId: 'api.v1.test', + tags: ['Test Routes'], + parameters: [], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/SimpleRouteResponse', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + SimpleRouteResponse: { + description: 'Human readable description of the Simple Route Response', + properties: { + test: { + type: 'array', + items: { + allOf: [{ $ref: '#/components/schemas/TestRefBase' }], + description: 'List of test refs', + }, + }, + }, + required: ['test'], + title: 'Human Readable Simple Route Response', + type: 'object', + }, + TestRefBase: { + title: 'TestRefBase', + type: 'string', + }, + TestRef: { + allOf: [ + { + title: 'TestRef', + }, + { + $ref: '#/components/schemas/TestRefBase', + }, + ], + }, + }, + }, + }, +);