Skip to content

Commit 2659bc3

Browse files
feat: support per-schema @ContentType JSDoc tags
1 parent b97760f commit 2659bc3

File tree

2 files changed

+235
-9
lines changed

2 files changed

+235
-9
lines changed

packages/openapi-generator/src/openapi.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,9 +346,20 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
346346
? {}
347347
: {
348348
requestBody: {
349-
content: {
350-
[contentType]: { schema: schemaToOpenAPI(route.body) },
351-
},
349+
content: (() => {
350+
const emptyBlock: Block = {
351+
description: '',
352+
tags: [],
353+
source: [],
354+
problems: [],
355+
};
356+
const bodyJsdoc = parseCommentBlock(route.body.comment ?? emptyBlock);
357+
const requestContentType = bodyJsdoc.tags?.contentType ?? contentType;
358+
359+
return {
360+
[requestContentType]: { schema: schemaToOpenAPI(route.body) },
361+
};
362+
})(),
352363
},
353364
};
354365

@@ -394,12 +405,21 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
394405
responses: Object.entries(route.response).reduce((acc, [code, response]) => {
395406
const description = STATUS_CODES[code] ?? '';
396407

408+
const emptyBlock: Block = {
409+
description: '',
410+
tags: [],
411+
source: [],
412+
problems: [],
413+
};
414+
const responseJsdoc = parseCommentBlock(response.comment ?? emptyBlock);
415+
const responseContentType = responseJsdoc.tags?.contentType ?? contentType;
416+
397417
return {
398418
...acc,
399419
[Number(code)]: {
400420
description,
401421
content: {
402-
[contentType]: {
422+
[responseContentType]: {
403423
schema: schemaToOpenAPI(response),
404424
...(example !== undefined ? { example } : undefined),
405425
},

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

Lines changed: 211 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,22 +293,29 @@ import * as t from 'io-ts';
293293
import * as h from '@api-ts/io-ts-http';
294294
295295
/**
296-
* Route with multipart/form-data content type
296+
* Route with per-schema content types
297297
*
298-
* @contentType multipart/form-data
299298
* @operationId api.v1.uploadDocument
300299
* @tag Document Upload
301300
*/
302301
export const uploadRoute = h.httpRoute({
303302
path: '/upload',
304303
method: 'POST',
305304
request: h.httpRequest({
305+
/**
306+
* File upload data
307+
* @contentType multipart/form-data
308+
*/
306309
body: t.type({
307310
file: t.unknown,
308311
documentType: t.string,
309312
}),
310313
}),
311314
response: {
315+
/**
316+
* Upload success response
317+
* @contentType application/xml
318+
*/
312319
201: t.type({
313320
id: t.string,
314321
success: t.boolean,
@@ -340,7 +347,7 @@ export const createUserRoute = h.httpRoute({
340347
});
341348
`;
342349

343-
testCase('route with contentType tag uses multipart/form-data', CONTENT_TYPE_TEST, {
350+
testCase('route with per-schema contentType tags', CONTENT_TYPE_TEST, {
344351
openapi: '3.0.3',
345352
info: {
346353
title: 'Test',
@@ -349,14 +356,15 @@ testCase('route with contentType tag uses multipart/form-data', CONTENT_TYPE_TES
349356
paths: {
350357
'/upload': {
351358
post: {
352-
summary: 'Route with multipart/form-data content type',
359+
summary: 'Route with per-schema content types',
353360
operationId: 'api.v1.uploadDocument',
354361
tags: ['Document Upload'],
355362
parameters: [],
356363
requestBody: {
357364
content: {
358365
'multipart/form-data': {
359366
schema: {
367+
description: 'File upload data',
360368
type: 'object',
361369
properties: {
362370
file: {},
@@ -371,8 +379,9 @@ testCase('route with contentType tag uses multipart/form-data', CONTENT_TYPE_TES
371379
201: {
372380
description: 'Created',
373381
content: {
374-
'multipart/form-data': {
382+
'application/xml': {
375383
schema: {
384+
description: 'Upload success response',
376385
type: 'object',
377386
properties: {
378387
id: { type: 'string' },
@@ -428,3 +437,200 @@ testCase('route with contentType tag uses multipart/form-data', CONTENT_TYPE_TES
428437
},
429438
components: { schemas: {} },
430439
});
440+
441+
const PER_RESPONSE_CONTENT_TYPE_TEST = `
442+
import * as t from 'io-ts';
443+
import * as h from '@api-ts/io-ts-http';
444+
445+
/**
446+
* Upload document with different response content types per status code
447+
* @operationId api.v1.uploadDocumentAdvanced
448+
* @tag Document Upload
449+
*/
450+
export const uploadAdvancedRoute = h.httpRoute({
451+
path: '/upload-advanced',
452+
method: 'POST',
453+
request: h.httpRequest({
454+
/**
455+
* Multipart form data for file upload
456+
* @contentType multipart/form-data
457+
*/
458+
body: t.type({
459+
file: t.unknown,
460+
documentType: t.string,
461+
}),
462+
}),
463+
response: {
464+
/**
465+
* Success response with JSON data
466+
* @contentType application/json
467+
*/
468+
200: t.type({
469+
id: t.string,
470+
success: t.boolean,
471+
}),
472+
473+
/**
474+
* Plain text error message
475+
* @contentType text/plain
476+
*/
477+
400: t.string,
478+
479+
/**
480+
* File download response
481+
* @contentType application/octet-stream
482+
*/
483+
201: t.unknown,
484+
},
485+
});
486+
487+
/**
488+
* Standard route with default content types
489+
* @operationId api.v1.createUserStandard
490+
* @tag User Management
491+
*/
492+
export const createUserStandardRoute = h.httpRoute({
493+
path: '/users-standard',
494+
method: 'POST',
495+
request: h.httpRequest({
496+
body: t.type({
497+
name: t.string,
498+
email: t.string,
499+
}),
500+
}),
501+
response: {
502+
200: t.type({
503+
id: t.string,
504+
name: t.string,
505+
}),
506+
400: t.type({
507+
error: t.string,
508+
}),
509+
},
510+
});
511+
`;
512+
513+
testCase('routes with per-response content types', PER_RESPONSE_CONTENT_TYPE_TEST, {
514+
openapi: '3.0.3',
515+
info: {
516+
title: 'Test',
517+
version: '1.0.0',
518+
},
519+
paths: {
520+
'/upload-advanced': {
521+
post: {
522+
summary:
523+
'Upload document with different response content types per status code',
524+
operationId: 'api.v1.uploadDocumentAdvanced',
525+
tags: ['Document Upload'],
526+
parameters: [],
527+
requestBody: {
528+
content: {
529+
'multipart/form-data': {
530+
schema: {
531+
description: 'Multipart form data for file upload',
532+
type: 'object',
533+
properties: {
534+
file: {},
535+
documentType: { type: 'string' },
536+
},
537+
required: ['file', 'documentType'],
538+
},
539+
},
540+
},
541+
},
542+
responses: {
543+
200: {
544+
description: 'OK',
545+
content: {
546+
'application/json': {
547+
schema: {
548+
description: 'Success response with JSON data',
549+
type: 'object',
550+
properties: {
551+
id: { type: 'string' },
552+
success: { type: 'boolean' },
553+
},
554+
required: ['id', 'success'],
555+
},
556+
},
557+
},
558+
},
559+
201: {
560+
description: 'Created',
561+
content: {
562+
'application/octet-stream': {
563+
schema: {},
564+
},
565+
},
566+
},
567+
400: {
568+
description: 'Bad Request',
569+
content: {
570+
'text/plain': {
571+
schema: {
572+
description: 'Plain text error message',
573+
type: 'string',
574+
},
575+
},
576+
},
577+
},
578+
},
579+
},
580+
},
581+
'/users-standard': {
582+
post: {
583+
summary: 'Standard route with default content types',
584+
operationId: 'api.v1.createUserStandard',
585+
tags: ['User Management'],
586+
parameters: [],
587+
requestBody: {
588+
content: {
589+
'application/json': {
590+
schema: {
591+
type: 'object',
592+
properties: {
593+
name: { type: 'string' },
594+
email: { type: 'string' },
595+
},
596+
required: ['name', 'email'],
597+
},
598+
},
599+
},
600+
},
601+
responses: {
602+
200: {
603+
description: 'OK',
604+
content: {
605+
'application/json': {
606+
schema: {
607+
type: 'object',
608+
properties: {
609+
id: { type: 'string' },
610+
name: { type: 'string' },
611+
},
612+
required: ['id', 'name'],
613+
},
614+
},
615+
},
616+
},
617+
400: {
618+
description: 'Bad Request',
619+
content: {
620+
'application/json': {
621+
schema: {
622+
type: 'object',
623+
properties: {
624+
error: { type: 'string' },
625+
},
626+
required: ['error'],
627+
},
628+
},
629+
},
630+
},
631+
},
632+
},
633+
},
634+
},
635+
components: { schemas: {} },
636+
});

0 commit comments

Comments
 (0)