Skip to content

Commit cb82337

Browse files
committed
fix: pass default openapi object to any case
Ticket: DX-2582 This commit passes the defaultOpenAPIObject to the any case. This allows for us to not drop the jsdoc information on t.any types. It is valid openapi to have descriptions and other metadata on any types: https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type From the following slack thread: https://bitgo.slack.com/archives/C057BHBRG4B/p1765481932730759 \# Test Tested in `entity-validation`: ``` $ > diff test.json test2.json 3123,3137c3123,3125 < "frontPhoto": { < "description": "Front photo of the identity document", < "example": "\"passport-front.jpg\"", < "format": "binary" < }, < "backPhoto": { < "description": "Back photo of the identity document", < "example": "\"drivers-license-back.png\"", < "format": "binary" < }, < "proofOfResidency": { < "description": "Proof of residency", < "example": "\"rental-lease.pdf\"", < "format": "binary" < } --- > "frontPhoto": {}, > "backPhoto": {}, > "proofOfResidency": {} 3264,3278c3252,3254 < "frontPhoto": { < "description": "Front photo of the identity document", < "example": "\"passport-front.jpg\"", < "format": "binary" < }, < "backPhoto": { < "description": "Back photo of the identity document", < "example": "\"drivers-license-back.png\"", < "format": "binary" < }, < "proofOfResidency": { < "description": "Proof of residency", < "example": "\"rental-lease.pdf\"", < "format": "binary" < } --- > "frontPhoto": {}, > "backPhoto": {}, > "proofOfResidency": {} ```
1 parent 71e7be9 commit cb82337

2 files changed

Lines changed: 175 additions & 62 deletions

File tree

packages/openapi-generator/src/openapi.ts

Lines changed: 47 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,14 @@ export function schemaToOpenAPI(
100100
return {
101101
type: 'object',
102102
...defaultOpenAPIObject,
103-
properties: Object.entries(schema.properties).reduce(
104-
(acc, [name, prop]) => {
105-
const innerSchema = schemaToOpenAPI(prop);
106-
if (innerSchema === undefined) {
107-
return acc;
108-
}
103+
properties: Object.entries(schema.properties).reduce((acc, [name, prop]) => {
104+
const innerSchema = schemaToOpenAPI(prop);
105+
if (innerSchema === undefined) {
106+
return acc;
107+
}
109108

110-
return { ...acc, [name]: innerSchema };
111-
},
112-
{} as Record<string, OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject>,
113-
),
109+
return { ...acc, [name]: innerSchema };
110+
}, {} as Record<string, OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject>),
114111
...(schema.required.length > 0 ? { required: schema.required } : {}),
115112
};
116113
case 'intersection':
@@ -234,7 +231,7 @@ export function schemaToOpenAPI(
234231
case 'undefined':
235232
return undefined;
236233
case 'any':
237-
return {};
234+
return { ...defaultOpenAPIObject };
238235
default:
239236
return {};
240237
}
@@ -429,60 +426,48 @@ export function convertRoutesToOpenAPI(
429426
routes: Route[],
430427
schemas: Record<string, Schema>,
431428
): OpenAPIV3.Document {
432-
const paths = routes.reduce(
433-
(acc, route) => {
434-
const [path, method, pathItem] = routeToOpenAPI(route);
435-
let pathObject = acc[path] ?? {};
436-
pathObject[method] = pathItem;
437-
return { ...acc, [path]: pathObject };
438-
},
439-
{} as Record<string, Record<string, OpenAPIV3.PathItemObject>>,
440-
);
441-
442-
const openapiSchemas = Object.entries(schemas).reduce(
443-
(acc, [name, schema]) => {
444-
const openapiSchema = schemaToOpenAPI(schema);
445-
if (openapiSchema === undefined) {
446-
return acc;
447-
} else if ('$ref' in openapiSchema) {
448-
return {
449-
...acc,
450-
[name]: {
451-
allOf: [{ title: name }, openapiSchema],
452-
},
453-
};
454-
} else {
455-
return {
456-
...acc,
457-
[name]: {
458-
title: name,
459-
...openapiSchema,
460-
},
461-
};
462-
}
463-
},
464-
{} as Record<string, OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject>,
465-
);
429+
const paths = routes.reduce((acc, route) => {
430+
const [path, method, pathItem] = routeToOpenAPI(route);
431+
let pathObject = acc[path] ?? {};
432+
pathObject[method] = pathItem;
433+
return { ...acc, [path]: pathObject };
434+
}, {} as Record<string, Record<string, OpenAPIV3.PathItemObject>>);
435+
436+
const openapiSchemas = Object.entries(schemas).reduce((acc, [name, schema]) => {
437+
const openapiSchema = schemaToOpenAPI(schema);
438+
if (openapiSchema === undefined) {
439+
return acc;
440+
} else if ('$ref' in openapiSchema) {
441+
return {
442+
...acc,
443+
[name]: {
444+
allOf: [{ title: name }, openapiSchema],
445+
},
446+
};
447+
} else {
448+
return {
449+
...acc,
450+
[name]: {
451+
title: name,
452+
...openapiSchema,
453+
},
454+
};
455+
}
456+
}, {} as Record<string, OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject>);
466457

467458
const sortedPaths = Object.keys(paths)
468459
.sort((a, b) => a.localeCompare(b))
469-
.reduce(
470-
(acc, key) => {
471-
const sortedMethods = Object.keys(paths[key]!)
472-
.sort((a, b) => a.localeCompare(b))
473-
.reduce(
474-
(methodAcc, methodKey) => {
475-
methodAcc[methodKey] = paths[key]![methodKey]!;
476-
return methodAcc;
477-
},
478-
{} as Record<string, OpenAPIV3.PathItemObject>,
479-
);
480-
481-
acc[key] = sortedMethods;
482-
return acc;
483-
},
484-
{} as Record<string, OpenAPIV3.PathItemObject>,
485-
);
460+
.reduce((acc, key) => {
461+
const sortedMethods = Object.keys(paths[key]!)
462+
.sort((a, b) => a.localeCompare(b))
463+
.reduce((methodAcc, methodKey) => {
464+
methodAcc[methodKey] = paths[key]![methodKey]!;
465+
return methodAcc;
466+
}, {} as Record<string, OpenAPIV3.PathItemObject>);
467+
468+
acc[key] = sortedMethods;
469+
return acc;
470+
}, {} as Record<string, OpenAPIV3.PathItemObject>);
486471

487472
return {
488473
openapi: '3.0.3',

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

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,3 +1232,131 @@ testCase(
12321232
},
12331233
},
12341234
);
1235+
1236+
const ROUTE_WITH_ANY_AND_DESCRIPTION = `
1237+
import * as t from 'io-ts';
1238+
import * as h from '@api-ts/io-ts-http';
1239+
1240+
/**
1241+
* A simple route
1242+
*
1243+
* @operationId api.v1.test
1244+
* @tag Test Routes
1245+
*/
1246+
export const route = h.httpRoute({
1247+
path: '/foo',
1248+
method: 'GET',
1249+
request: h.httpRequest({}),
1250+
response: {
1251+
200: {
1252+
/**
1253+
* Test description
1254+
*/
1255+
test: t.any
1256+
}
1257+
},
1258+
});
1259+
`;
1260+
1261+
testCase('route with example object', ROUTE_WITH_ANY_AND_DESCRIPTION, {
1262+
openapi: '3.0.3',
1263+
info: {
1264+
title: 'Test',
1265+
version: '1.0.0',
1266+
},
1267+
paths: {
1268+
'/foo': {
1269+
get: {
1270+
summary: 'A simple route',
1271+
operationId: 'api.v1.test',
1272+
tags: ['Test Routes'],
1273+
parameters: [],
1274+
responses: {
1275+
200: {
1276+
description: 'OK',
1277+
content: {
1278+
'application/json': {
1279+
schema: {
1280+
type: 'object',
1281+
properties: {
1282+
test: {
1283+
description: 'Test description',
1284+
},
1285+
},
1286+
required: ['test'],
1287+
},
1288+
},
1289+
},
1290+
},
1291+
},
1292+
},
1293+
},
1294+
},
1295+
components: {
1296+
schemas: {},
1297+
},
1298+
});
1299+
1300+
const ROUTE_WITH_ANY_AND_FORMAT = `
1301+
import * as t from 'io-ts';
1302+
import * as h from '@api-ts/io-ts-http';
1303+
1304+
/**
1305+
* A simple route
1306+
*
1307+
* @operationId api.v1.test
1308+
* @tag Test Routes
1309+
*/
1310+
export const route = h.httpRoute({
1311+
path: '/foo',
1312+
method: 'GET',
1313+
request: h.httpRequest({}),
1314+
response: {
1315+
200: {
1316+
/**
1317+
* @format binary
1318+
*/
1319+
test: t.any
1320+
}
1321+
},
1322+
});
1323+
`;
1324+
1325+
testCase('route with example object', ROUTE_WITH_ANY_AND_FORMAT, {
1326+
openapi: '3.0.3',
1327+
info: {
1328+
title: 'Test',
1329+
version: '1.0.0',
1330+
},
1331+
paths: {
1332+
'/foo': {
1333+
get: {
1334+
summary: 'A simple route',
1335+
operationId: 'api.v1.test',
1336+
tags: ['Test Routes'],
1337+
parameters: [],
1338+
responses: {
1339+
200: {
1340+
description: 'OK',
1341+
content: {
1342+
'application/json': {
1343+
schema: {
1344+
type: 'object',
1345+
properties: {
1346+
test: {
1347+
format: 'binary',
1348+
},
1349+
},
1350+
required: ['test'],
1351+
},
1352+
},
1353+
},
1354+
},
1355+
},
1356+
},
1357+
},
1358+
},
1359+
components: {
1360+
schemas: {},
1361+
},
1362+
});

0 commit comments

Comments
 (0)