From 26f18f1fb9262200e8a0dfd98398ee4dbb808218 Mon Sep 17 00:00:00 2001 From: Aryaman Dhingra Date: Tue, 28 May 2024 13:39:39 -0400 Subject: [PATCH 1/3] feat(openapi-generator): add support for inline type descriptions DX-427 --- packages/openapi-generator/src/openapi.ts | 36 ++++++++++++++++--- .../openapi-generator/test/openapi.test.ts | 2 ++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/openapi-generator/src/openapi.ts b/packages/openapi-generator/src/openapi.ts index 2350221d..a53dd519 100644 --- a/packages/openapi-generator/src/openapi.ts +++ b/packages/openapi-generator/src/openapi.ts @@ -14,13 +14,27 @@ function schemaToOpenAPI( const createOpenAPIObject = ( schema: Schema, ): OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined => { + const description = schema.comment?.description; + + const defaultObject = { + ...(description ? { description } : {}), + }; + switch (schema.type) { case 'boolean': case 'string': case 'number': - return { type: schema.type, ...(schema.enum ? { enum: schema.enum } : {}) }; + return { + type: schema.type, + ...(schema.enum ? { enum: schema.enum } : {}), + ...defaultObject, + }; case 'integer': - return { type: 'number', ...(schema.enum ? { enum: schema.enum } : {}) }; + return { + type: 'number', + ...(schema.enum ? { enum: schema.enum } : {}), + ...defaultObject, + }; case 'null': // TODO: OpenAPI v3 does not have an explicit null type, is there a better way to represent this? // Or should we just conflate explicit null and undefined properties? @@ -32,7 +46,7 @@ function schemaToOpenAPI( if (innerSchema === undefined) { return undefined; } - return { type: 'array', items: innerSchema }; + return { type: 'array', items: innerSchema, ...defaultObject }; case 'object': return { type: 'object', @@ -146,13 +160,25 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec {}, ); + const stripTopLevelComment = (schema: Schema) => { + const copy = { ...schema }; + + if (copy.comment?.description !== undefined && copy.comment?.description !== '') { + copy.comment.description = ''; + } + + return copy; + }; + + const topLevelStripped = stripTopLevelComment(route.body!); + const requestBody = route.body === undefined ? {} : { requestBody: { content: { - 'application/json': { schema: schemaToOpenAPI(route.body) }, + 'application/json': { schema: schemaToOpenAPI(topLevelStripped) }, }, }, }; @@ -195,7 +221,7 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec description, content: { 'application/json': { - schema: schemaToOpenAPI(response), + schema: schemaToOpenAPI(stripTopLevelComment(response)), ...(example !== undefined ? { example } : undefined), }, }, diff --git a/packages/openapi-generator/test/openapi.test.ts b/packages/openapi-generator/test/openapi.test.ts index e5f9e269..6e2ccc8c 100644 --- a/packages/openapi-generator/test/openapi.test.ts +++ b/packages/openapi-generator/test/openapi.test.ts @@ -161,6 +161,7 @@ testCase('simple route', SIMPLE, { description: 'foo param', required: true, schema: { + description: 'foo param', type: 'string', }, }, @@ -927,6 +928,7 @@ testCase('schema parameter with title tag', TITLE_TAG, { description: 'bar param', required: true, schema: { + description: 'bar param', title: 'this is a bar parameter', type: 'string', }, From 2365e36205cf46e863ea79d051d7f092aeb84ba9 Mon Sep 17 00:00:00 2001 From: Aryaman Dhingra Date: Tue, 28 May 2024 13:53:22 -0400 Subject: [PATCH 2/3] test(openapi-generator): add new test with type descriptions --- .../openapi-generator/test/openapi.test.ts | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/packages/openapi-generator/test/openapi.test.ts b/packages/openapi-generator/test/openapi.test.ts index 6e2ccc8c..ba715ec7 100644 --- a/packages/openapi-generator/test/openapi.test.ts +++ b/packages/openapi-generator/test/openapi.test.ts @@ -1333,3 +1333,112 @@ testCase('route with multiple unknown tags', ROUTE_WITH_MULTIPLE_UNKNOWN_TAGS, { schemas: {}, }, }); + + +const ROUTE_WITH_TYPE_DESCRIPTIONS = ` +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({ + body: { + /** foo description */ + foo: t.string, + /** bar description */ + bar: t.number, + child: { + /** child description */ + child: t.string, + } + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with type descriptions', ROUTE_WITH_TYPE_DESCRIPTIONS, { + 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: [], + requestBody: { + content: { + 'application/json': { + schema: { + properties: { + bar: { + description: 'bar description', + type: 'number' + }, + child: { + properties: { + child: { + description: 'child description', + type: 'string' + } + }, + required: [ + 'child' + ], + type: 'object' + }, + foo: { + description: 'foo description', + type: 'string' + } + }, + required: [ + 'foo', + 'bar', + 'child' + ], + type: 'object' + } + } + } + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, + required: ['test'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); From bdb071a7b5777a394ab771462d580b7d08e55bb3 Mon Sep 17 00:00:00 2001 From: Aryaman Dhingra Date: Tue, 28 May 2024 14:19:43 -0400 Subject: [PATCH 3/3] fix(openapi-generator): remove duplicate descriptions in endpoint parameters DX-427 --- packages/openapi-generator/src/openapi.ts | 4 ++++ .../openapi-generator/test/openapi.test.ts | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/openapi-generator/src/openapi.ts b/packages/openapi-generator/src/openapi.ts index a53dd519..d910d69f 100644 --- a/packages/openapi-generator/src/openapi.ts +++ b/packages/openapi-generator/src/openapi.ts @@ -200,6 +200,10 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec // Array types not allowed here const schema = schemaToOpenAPI(p.schema); + if (schema && 'description' in schema) { + delete schema.description; + } + return { name: p.name, ...(p.schema?.comment?.description !== undefined diff --git a/packages/openapi-generator/test/openapi.test.ts b/packages/openapi-generator/test/openapi.test.ts index ba715ec7..b0792348 100644 --- a/packages/openapi-generator/test/openapi.test.ts +++ b/packages/openapi-generator/test/openapi.test.ts @@ -161,7 +161,6 @@ testCase('simple route', SIMPLE, { description: 'foo param', required: true, schema: { - description: 'foo param', type: 'string', }, }, @@ -928,7 +927,6 @@ testCase('schema parameter with title tag', TITLE_TAG, { description: 'bar param', required: true, schema: { - description: 'bar param', title: 'this is a bar parameter', type: 'string', }, @@ -1349,6 +1347,10 @@ export const route = h.httpRoute({ path: '/foo', method: 'GET', request: h.httpRequest({ + query: { + /** bar param */ + bar: t.string, + }, body: { /** foo description */ foo: t.string, @@ -1380,7 +1382,17 @@ testCase('route with type descriptions', ROUTE_WITH_TYPE_DESCRIPTIONS, { summary: 'A simple route with type descriptions', operationId: 'api.v1.test', tags: ['Test Routes'], - parameters: [], + parameters: [ + { + description: 'bar param', + in: 'query', + name: 'bar', + required: true, + schema: { + type: 'string' + } + } + ], requestBody: { content: { 'application/json': {