Skip to content

Commit c676efb

Browse files
authored
Merge pull request #795 from BitGo/DX-475-optional-unions
feat(openapi-generator): add support for optional array doc generation
2 parents 2fba57f + bd03d06 commit c676efb

File tree

2 files changed

+119
-17
lines changed

2 files changed

+119
-17
lines changed

packages/openapi-generator/src/openapi.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function schemaToOpenAPI(
4949
if (innerSchema === undefined) {
5050
return undefined;
5151
}
52-
return { type: 'array', items: innerSchema, ...defaultOpenAPIObject };
52+
return { type: 'array', items: { ...innerSchema, ...defaultOpenAPIObject } };
5353
case 'object':
5454
return {
5555
type: 'object',
@@ -81,6 +81,17 @@ function schemaToOpenAPI(
8181
case 'union':
8282
let nullable = false;
8383
let oneOf: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[] = [];
84+
85+
// If there are two schemas and one of the schemas is undefined, that means the union is a case of `optional` type
86+
const undefinedSchema = schema.schemas.find((s) => s.type === 'undefined');
87+
const nonUndefinedSchema = schema.schemas.find((s) => s.type !== 'undefined');
88+
// and we can just return the other schema (while attaching the comment to that schema)
89+
const isOptional =
90+
schema.schemas.length == 2 && undefinedSchema && nonUndefinedSchema;
91+
if (isOptional) {
92+
return schemaToOpenAPI({ ...nonUndefinedSchema, comment: schema.comment });
93+
}
94+
8495
for (const s of schema.schemas) {
8596
if (s.type === 'null') {
8697
nullable = true;
@@ -171,6 +182,7 @@ function schemaToOpenAPI(
171182
const readOnly = getTagName(schema, 'readOnly');
172183
const writeOnly = getTagName(schema, 'writeOnly');
173184
const format = getTagName(schema, 'format');
185+
const title = getTagContent(schema, 'title');
174186

175187
const deprecated = schema.comment?.tags.find((t) => t.tag === 'deprecated');
176188
const description = schema.comment?.description;
@@ -196,28 +208,29 @@ function schemaToOpenAPI(
196208
...(readOnly ? { readOnly: true } : {}),
197209
...(writeOnly ? { writeOnly: true } : {}),
198210
...(format ? { format } : {}),
211+
...(title ? { title } : {}),
199212
};
200213
return defaultOpenAPIObject;
201214
}
202215

203-
const titleObject = schema.comment?.tags.find((t) => t.tag === 'title');
204-
205216
let openAPIObject = createOpenAPIObject(schema);
206217

207-
if (titleObject !== undefined) {
208-
openAPIObject = {
209-
...openAPIObject,
210-
title: `${titleObject.name} ${titleObject.description}`.trim(),
211-
};
212-
}
213-
214218
return openAPIObject;
215219
}
216220

217221
function getTagName(schema: Schema, tagName: String): string | undefined {
218222
return schema.comment?.tags.find((t) => t.tag === tagName)?.name;
219223
}
220224

225+
function getTagContent(schema: Schema, tagName: String): string | undefined {
226+
if (schema.comment === undefined) return undefined;
227+
228+
const tag = schema.comment.tags.find((t) => t.tag === tagName);
229+
if (tag === undefined) return undefined;
230+
231+
return `${tag.name} ${tag.description}`.trim();
232+
}
233+
221234
function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObject] {
222235
const jsdoc = route.comment !== undefined ? parseCommentBlock(route.comment) : {};
223236
const operationId = jsdoc.tags?.operationId;

packages/openapi-generator/test/openapi.test.ts

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,16 +1828,16 @@ testCase('route with array types and descriptions', ROUTE_WITH_ARRAY_TYPES_AND_D
18281828
foo: {
18291829
type: 'array',
18301830
items: {
1831-
type: 'string'
1831+
type: 'string',
1832+
description: 'foo description'
18321833
},
1833-
description: 'foo description'
18341834
},
18351835
bar: {
18361836
type: 'array',
18371837
items: {
1838-
type: 'number'
1838+
type: 'number',
1839+
description: 'bar description'
18391840
},
1840-
description: 'bar description'
18411841
},
18421842
child: {
18431843
type: 'object',
@@ -1852,9 +1852,9 @@ testCase('route with array types and descriptions', ROUTE_WITH_ARRAY_TYPES_AND_D
18521852
{
18531853
type: 'number'
18541854
}
1855-
]
1855+
],
1856+
description: 'child description'
18561857
},
1857-
description: 'child description'
18581858
}
18591859
},
18601860
required: [
@@ -2128,6 +2128,7 @@ testCase('route with descriptions, patterns, and examples', ROUTE_WITH_DESCRIPTI
21282128
child: {
21292129
type: 'array',
21302130
items: {
2131+
description: 'child description',
21312132
oneOf: [
21322133
{
21332134
type: 'string'
@@ -2137,7 +2138,6 @@ testCase('route with descriptions, patterns, and examples', ROUTE_WITH_DESCRIPTI
21372138
}
21382139
]
21392140
},
2140-
description: 'child description'
21412141
}
21422142
},
21432143
required: [
@@ -2645,3 +2645,92 @@ testCase('route with min and max tags', ROUTE_WITH_MIN_MAX_AND_OTHER_TAGS, {
26452645
schemas: {}
26462646
}
26472647
});
2648+
2649+
const ROUTE_WITH_ARRAY_QUERY_PARAM = `
2650+
import * as t from 'io-ts';
2651+
import * as h from '@api-ts/io-ts-http';
2652+
2653+
/**
2654+
* A simple route with type descriptions for references
2655+
*
2656+
* @operationId api.v1.test
2657+
* @tag Test Routes
2658+
*/
2659+
export const route = h.httpRoute({
2660+
path: '/foo',
2661+
method: 'GET',
2662+
request: h.httpRequest({
2663+
query: {
2664+
/**
2665+
* This is a foo description.
2666+
* @example "abc"
2667+
* @pattern ^[a-z]+$
2668+
*/
2669+
foo: h.optional(t.array(t.string))
2670+
},
2671+
}),
2672+
response: {
2673+
200: {
2674+
test: t.string
2675+
}
2676+
},
2677+
});
2678+
`;
2679+
2680+
testCase('route with optional array query parameter and documentation', ROUTE_WITH_ARRAY_QUERY_PARAM, {
2681+
openapi: '3.0.3',
2682+
info: {
2683+
title: 'Test',
2684+
version: '1.0.0'
2685+
},
2686+
paths: {
2687+
'/foo': {
2688+
get: {
2689+
summary: 'A simple route with type descriptions for references',
2690+
operationId: 'api.v1.test',
2691+
tags: [
2692+
'Test Routes'
2693+
],
2694+
parameters: [
2695+
{
2696+
description: 'This is a foo description.',
2697+
in: 'query',
2698+
name: 'foo',
2699+
schema: {
2700+
items: {
2701+
description: 'This is a foo description.',
2702+
example: 'abc',
2703+
type: 'string',
2704+
pattern: '^[a-z]+$'
2705+
},
2706+
type: 'array'
2707+
}
2708+
}
2709+
],
2710+
responses: {
2711+
'200': {
2712+
description: 'OK',
2713+
content: {
2714+
'application/json': {
2715+
schema: {
2716+
type: 'object',
2717+
properties: {
2718+
test: {
2719+
type: 'string'
2720+
}
2721+
},
2722+
required: [
2723+
'test'
2724+
]
2725+
}
2726+
}
2727+
}
2728+
}
2729+
}
2730+
}
2731+
}
2732+
},
2733+
components: {
2734+
schemas: {}
2735+
}
2736+
});

0 commit comments

Comments
 (0)