Skip to content

Commit

Permalink
feat: support for @example and @pattern tags in schema properties
Browse files Browse the repository at this point in the history
  • Loading branch information
anshchaturvedi committed May 31, 2024
1 parent b93dfb0 commit f9d9ac8
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 10 deletions.
32 changes: 22 additions & 10 deletions packages/openapi-generator/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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?
Expand All @@ -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);
Expand All @@ -73,7 +77,7 @@ function schemaToOpenAPI(
}
return [innerSchema];
}),
...defaultObject,
...defaultOpenAPIObject,
};
case 'union':
let nullable = false;
Expand All @@ -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);
Expand All @@ -115,7 +123,7 @@ function schemaToOpenAPI(
return {
type: 'object',
additionalProperties,
...defaultObject,
...defaultOpenAPIObject,
};
case 'undefined':
return undefined;
Expand All @@ -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;
Expand Down
143 changes: 143 additions & 0 deletions packages/openapi-generator/test/openapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}
}
});

0 comments on commit f9d9ac8

Please sign in to comment.