From f9d9ac8885f53e7721ab3536def17f9437626641 Mon Sep 17 00:00:00 2001 From: Ansh Chaturvedi Date: Fri, 31 May 2024 16:52:44 -0400 Subject: [PATCH] feat: support for `@example` and `@pattern` tags in schema properties --- packages/openapi-generator/src/openapi.ts | 32 ++-- .../openapi-generator/test/openapi.test.ts | 143 ++++++++++++++++++ 2 files changed, 165 insertions(+), 10 deletions(-) diff --git a/packages/openapi-generator/src/openapi.ts b/packages/openapi-generator/src/openapi.ts index 67b631f8..c9efd035 100644 --- a/packages/openapi-generator/src/openapi.ts +++ b/packages/openapi-generator/src/openapi.ts @@ -15,9 +15,13 @@ function schemaToOpenAPI( schema: Schema, ): OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined => { const description = schema.comment?.description; + const example = getTagName(schema, 'example'); + const pattern = getTagName(schema, 'pattern'); - const defaultObject = { + const defaultOpenAPIObject = { ...(description ? { description } : {}), + ...(example ? { example } : {}), + ...(pattern ? { pattern } : {}), }; switch (schema.type) { @@ -27,13 +31,13 @@ function schemaToOpenAPI( return { type: schema.type, ...(schema.enum ? { enum: schema.enum } : {}), - ...defaultObject, + ...defaultOpenAPIObject, }; case 'integer': return { type: 'number', ...(schema.enum ? { enum: schema.enum } : {}), - ...defaultObject, + ...defaultOpenAPIObject, }; case 'null': // TODO: OpenAPI v3 does not have an explicit null type, is there a better way to represent this? @@ -46,11 +50,11 @@ function schemaToOpenAPI( if (innerSchema === undefined) { return undefined; } - return { type: 'array', items: innerSchema, ...defaultObject }; + return { type: 'array', items: innerSchema, ...defaultOpenAPIObject }; case 'object': return { type: 'object', - ...defaultObject, + ...defaultOpenAPIObject, properties: Object.entries(schema.properties).reduce( (acc, [name, prop]) => { const innerSchema = schemaToOpenAPI(prop); @@ -73,7 +77,7 @@ function schemaToOpenAPI( } return [innerSchema]; }), - ...defaultObject, + ...defaultOpenAPIObject, }; case 'union': let nullable = false; @@ -100,12 +104,16 @@ function schemaToOpenAPI( return { ...(nullable ? { nullable } : {}), allOf: oneOf, - ...defaultObject, + ...defaultOpenAPIObject, }; else - return { ...(nullable ? { nullable } : {}), ...oneOf[0], ...defaultObject }; + return { + ...(nullable ? { nullable } : {}), + ...oneOf[0], + ...defaultOpenAPIObject, + }; } else { - return { ...(nullable ? { nullable } : {}), oneOf, ...defaultObject }; + return { ...(nullable ? { nullable } : {}), oneOf, ...defaultOpenAPIObject }; } case 'record': const additionalProperties = schemaToOpenAPI(schema.codomain); @@ -115,7 +123,7 @@ function schemaToOpenAPI( return { type: 'object', additionalProperties, - ...defaultObject, + ...defaultOpenAPIObject, }; case 'undefined': return undefined; @@ -140,6 +148,10 @@ function schemaToOpenAPI( return openAPIObject; } +function getTagName(schema: Schema, tagName: String): string | undefined { + return schema.comment?.tags.find((t) => t.tag === tagName)?.name; +} + 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 a239c707..88d18eb8 100644 --- a/packages/openapi-generator/test/openapi.test.ts +++ b/packages/openapi-generator/test/openapi.test.ts @@ -2036,3 +2036,146 @@ testCase('route with record types and descriptions', ROUTE_WITH_RECORD_TYPES_AND schemas: {} } }); + +const ROUTE_WITH_DESCRIPTIONS_PATTERNS_EXAMPLES = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** + * This is a bar param. + * @example { "foo": "bar" } + */ + bar: t.record(t.string, t.string), + }, + body: { + /** + * foo description + * @pattern ^[1-9][0-9]{4}$ + * @example 12345 + */ + foo: t.number, + child: { + /** + * child description + */ + child: t.array(t.union([t.string, t.number])), + } + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with descriptions, patterns, and examples', ROUTE_WITH_DESCRIPTIONS_PATTERNS_EXAMPLES, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions', + operationId: 'api.v1.test', + tags: [ + 'Test Routes' + ], + parameters: [ + { + name: 'bar', + description: 'This is a bar param.', + in: 'query', + required: true, + schema: { + type: 'object', + additionalProperties: { + type: 'string' + } + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + foo: { + type: 'number', + description: 'foo description', + example: '12345', + pattern: '^[1-9][0-9]{4}$' + }, + child: { + type: 'object', + properties: { + child: { + type: 'array', + items: { + oneOf: [ + { + type: 'string' + }, + { + type: 'number' + } + ] + }, + description: 'child description' + } + }, + required: [ + 'child' + ] + } + }, + required: [ + 'foo', + 'child' + ] + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +});