Skip to content

Commit

Permalink
feat(openapi-generator): add support for optional array doc generation
Browse files Browse the repository at this point in the history
  • Loading branch information
ad-world committed Jun 11, 2024
1 parent dcf4064 commit eaf302a
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 17 deletions.
33 changes: 23 additions & 10 deletions packages/openapi-generator/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -196,28 +208,29 @@ 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;
}

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;
Expand Down
103 changes: 96 additions & 7 deletions packages/openapi-generator/test/openapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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: [
Expand Down Expand Up @@ -2128,6 +2128,7 @@ testCase('route with descriptions, patterns, and examples', ROUTE_WITH_DESCRIPTI
child: {
type: 'array',
items: {
description: 'child description',
oneOf: [
{
type: 'string'
Expand All @@ -2137,7 +2138,6 @@ testCase('route with descriptions, patterns, and examples', ROUTE_WITH_DESCRIPTI
}
]
},
description: 'child description'
}
},
required: [
Expand Down Expand Up @@ -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: {}
}
}, [])

Check notice

Code scanning / CodeQL

Semicolon insertion Note test

Avoid automated semicolon insertion (98% of all statements in
the enclosing script
have an explicit semicolon).

0 comments on commit eaf302a

Please sign in to comment.