diff --git a/packages/openapi-generator/src/openapi.ts b/packages/openapi-generator/src/openapi.ts index 8901d63f..2bd0bb67 100644 --- a/packages/openapi-generator/src/openapi.ts +++ b/packages/openapi-generator/src/openapi.ts @@ -49,7 +49,7 @@ function schemaToOpenAPI( if (innerSchema === undefined) { return undefined; } - return { type: 'array', items: innerSchema, ...defaultOpenAPIObject }; + return { type: 'array', items: { ...innerSchema, ...defaultOpenAPIObject } }; case 'object': return { type: 'object', @@ -81,6 +81,17 @@ function schemaToOpenAPI( case 'union': let nullable = false; let oneOf: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[] = []; + + // If there are two schemas and one of the schemas is undefined, that means the union is a case of `optional` type + const undefinedSchema = schema.schemas.find((s) => s.type === 'undefined'); + const nonUndefinedSchema = schema.schemas.find((s) => s.type !== 'undefined'); + // and we can just return the other schema (while attaching the comment to that schema) + const isOptional = + schema.schemas.length == 2 && undefinedSchema && nonUndefinedSchema; + if (isOptional) { + return schemaToOpenAPI({ ...nonUndefinedSchema, comment: schema.comment }); + } + for (const s of schema.schemas) { if (s.type === 'null') { nullable = true; @@ -171,6 +182,7 @@ function schemaToOpenAPI( const readOnly = getTagName(schema, 'readOnly'); const writeOnly = getTagName(schema, 'writeOnly'); const format = getTagName(schema, 'format'); + const title = getTagContent(schema, 'title'); const deprecated = schema.comment?.tags.find((t) => t.tag === 'deprecated'); const description = schema.comment?.description; @@ -196,21 +208,13 @@ function schemaToOpenAPI( ...(readOnly ? { readOnly: true } : {}), ...(writeOnly ? { writeOnly: true } : {}), ...(format ? { format } : {}), + ...(title ? { title } : {}), }; return defaultOpenAPIObject; } - const titleObject = schema.comment?.tags.find((t) => t.tag === 'title'); - let openAPIObject = createOpenAPIObject(schema); - if (titleObject !== undefined) { - openAPIObject = { - ...openAPIObject, - title: `${titleObject.name} ${titleObject.description}`.trim(), - }; - } - return openAPIObject; } @@ -218,6 +222,15 @@ function getTagName(schema: Schema, tagName: String): string | undefined { return schema.comment?.tags.find((t) => t.tag === tagName)?.name; } +function getTagContent(schema: Schema, tagName: String): string | undefined { + if (schema.comment === undefined) return undefined; + + const tag = schema.comment.tags.find((t) => t.tag === tagName); + if (tag === undefined) return undefined; + + return `${tag.name} ${tag.description}`.trim(); +} + function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObject] { const jsdoc = route.comment !== undefined ? parseCommentBlock(route.comment) : {}; const operationId = jsdoc.tags?.operationId; diff --git a/packages/openapi-generator/test/openapi.test.ts b/packages/openapi-generator/test/openapi.test.ts index 2dbb5a26..5d137b6f 100644 --- a/packages/openapi-generator/test/openapi.test.ts +++ b/packages/openapi-generator/test/openapi.test.ts @@ -1828,16 +1828,16 @@ testCase('route with array types and descriptions', ROUTE_WITH_ARRAY_TYPES_AND_D foo: { type: 'array', items: { - type: 'string' + type: 'string', + description: 'foo description' }, - description: 'foo description' }, bar: { type: 'array', items: { - type: 'number' + type: 'number', + description: 'bar description' }, - description: 'bar description' }, child: { type: 'object', @@ -1852,9 +1852,9 @@ testCase('route with array types and descriptions', ROUTE_WITH_ARRAY_TYPES_AND_D { type: 'number' } - ] + ], + description: 'child description' }, - description: 'child description' } }, required: [ @@ -2128,6 +2128,7 @@ testCase('route with descriptions, patterns, and examples', ROUTE_WITH_DESCRIPTI child: { type: 'array', items: { + description: 'child description', oneOf: [ { type: 'string' @@ -2137,7 +2138,6 @@ testCase('route with descriptions, patterns, and examples', ROUTE_WITH_DESCRIPTI } ] }, - description: 'child description' } }, required: [ @@ -2645,3 +2645,92 @@ testCase('route with min and max tags', ROUTE_WITH_MIN_MAX_AND_OTHER_TAGS, { schemas: {} } }); + +const ROUTE_WITH_ARRAY_QUERY_PARAM = ` +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({ + query: { + /** + * This is a foo description. + * @example "abc" + * @pattern ^[a-z]+$ + */ + foo: h.optional(t.array(t.string)) + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with optional array query parameter and documentation', ROUTE_WITH_ARRAY_QUERY_PARAM, { + 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: [ + { + description: 'This is a foo description.', + in: 'query', + name: 'foo', + schema: { + items: { + description: 'This is a foo description.', + example: 'abc', + type: 'string', + pattern: '^[a-z]+$' + }, + type: 'array' + } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +});