Skip to content

Commit 6c5a7f6

Browse files
authoredJul 1, 2024··
Merge pull request #2835 from lit26/feature/add-response-decorator-from-comment
feat(introspectComments): Enhanced comment parsing for error response
2 parents 29adc2d + 3baf1de commit 6c5a7f6

File tree

3 files changed

+131
-10
lines changed

3 files changed

+131
-10
lines changed
 

‎lib/plugin/utils/ast-utils.ts

+39-10
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,18 @@ export function getDefaultTypeFormatFlags(enclosingNode: Node) {
138138
return formatFlags;
139139
}
140140

141-
export function getMainCommentOfNode(
142-
node: Node,
143-
sourceFile: SourceFile
144-
): string {
141+
export function getDocComment(node: Node): DocComment {
145142
const tsdocParser: TSDocParser = new TSDocParser();
146143
const parserContext: ParserContext = tsdocParser.parseString(
147144
node.getFullText()
148145
);
149-
const docComment: DocComment = parserContext.docComment;
146+
return parserContext.docComment;
147+
}
148+
export function getMainCommentOfNode(
149+
node: Node,
150+
sourceFile: SourceFile
151+
): string {
152+
const docComment = getDocComment(node);
150153
return renderDocNode(docComment.summarySection).trim();
151154
}
152155

@@ -168,11 +171,7 @@ export function parseCommentDocValue(docValue: string, type: ts.Type) {
168171
}
169172

170173
export function getTsDocTagsOfNode(node: Node, typeChecker: TypeChecker) {
171-
const tsdocParser: TSDocParser = new TSDocParser();
172-
const parserContext: ParserContext = tsdocParser.parseString(
173-
node.getFullText()
174-
);
175-
const docComment: DocComment = parserContext.docComment;
174+
const docComment = getDocComment(node);
176175

177176
const tagDefinitions: {
178177
[key: string]: {
@@ -228,6 +227,36 @@ export function getTsDocTagsOfNode(node: Node, typeChecker: TypeChecker) {
228227
return tagResults;
229228
}
230229

230+
export function getTsDocErrorsOfNode(node: Node) {
231+
const tsdocParser: TSDocParser = new TSDocParser();
232+
const parserContext: ParserContext = tsdocParser.parseString(
233+
node.getFullText()
234+
);
235+
const docComment: DocComment = parserContext.docComment;
236+
237+
const tagResults = [];
238+
const errorParsingRegex = /{(\d+)} (.*)/;
239+
240+
const introspectTsDocTags = (docComment: DocComment) => {
241+
const blocks = docComment.customBlocks.filter(
242+
(block) => block.blockTag.tagName === '@throws'
243+
);
244+
245+
blocks.forEach((block) => {
246+
try {
247+
const docValue = renderDocNode(block.content).split('\n')[0].trim();
248+
const match = docValue.match(errorParsingRegex);
249+
tagResults.push({
250+
status: match[1],
251+
description: `"${match[2]}"`
252+
});
253+
} catch (err) {}
254+
});
255+
};
256+
introspectTsDocTags(docComment);
257+
return tagResults;
258+
}
259+
231260
export function getDecoratorArguments(decorator: Decorator) {
232261
const callExpression = decorator.expression;
233262
return (callExpression && (callExpression as CallExpression).arguments) || [];

‎lib/plugin/visitors/controller-class.visitor.ts

+81
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getDecoratorArguments,
1010
getDecoratorName,
1111
getMainCommentOfNode,
12+
getTsDocErrorsOfNode,
1213
getTsDocTagsOfNode
1314
} from '../utils/ast-utils';
1415
import {
@@ -139,6 +140,17 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
139140
typeChecker,
140141
metadata
141142
);
143+
144+
const apiResponseDecoratorsArray = this.createApiResponseDecorator(
145+
factory,
146+
compilerNode,
147+
decorators,
148+
options,
149+
sourceFile,
150+
typeChecker,
151+
metadata
152+
);
153+
142154
const removeExistingApiOperationDecorator =
143155
apiOperationDecoratorsArray.length > 0;
144156

@@ -160,6 +172,7 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
160172
);
161173
const updatedDecorators = [
162174
...apiOperationDecoratorsArray,
175+
...apiResponseDecoratorsArray,
163176
...existingDecorators,
164177
factory.createDecorator(
165178
factory.createCallExpression(
@@ -302,6 +315,74 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
302315
}
303316
}
304317

318+
createApiResponseDecorator(
319+
factory: ts.NodeFactory,
320+
node: ts.MethodDeclaration,
321+
decorators: readonly ts.Decorator[],
322+
options: PluginOptions,
323+
sourceFile: ts.SourceFile,
324+
typeChecker: ts.TypeChecker,
325+
metadata: ClassMetadata
326+
) {
327+
if (!options.introspectComments) {
328+
return [];
329+
}
330+
const apiResponseDecorator = getDecoratorOrUndefinedByNames(
331+
[ApiResponse.name],
332+
decorators,
333+
factory
334+
);
335+
let apiResponseExistingProps:
336+
| ts.NodeArray<ts.PropertyAssignment>
337+
| undefined = undefined;
338+
339+
if (apiResponseDecorator && !options.readonly) {
340+
const apiResponseExpr = head(getDecoratorArguments(apiResponseDecorator));
341+
if (apiResponseExpr) {
342+
apiResponseExistingProps =
343+
apiResponseExpr.properties as ts.NodeArray<ts.PropertyAssignment>;
344+
}
345+
}
346+
347+
const tags = getTsDocErrorsOfNode(node);
348+
if (!tags.length) {
349+
return [];
350+
}
351+
352+
return tags.map((tag) => {
353+
const properties = [
354+
...(apiResponseExistingProps ?? factory.createNodeArray())
355+
];
356+
properties.push(
357+
factory.createPropertyAssignment(
358+
'status',
359+
factory.createNumericLiteral(tag.status)
360+
)
361+
);
362+
properties.push(
363+
factory.createPropertyAssignment(
364+
'description',
365+
factory.createNumericLiteral(tag.description)
366+
)
367+
);
368+
const objectLiteralExpr = factory.createObjectLiteralExpression(
369+
compact(properties)
370+
);
371+
const methodKey = node.name.getText();
372+
metadata[methodKey] = objectLiteralExpr;
373+
374+
const apiResponseDecoratorArguments: ts.NodeArray<ts.Expression> =
375+
factory.createNodeArray([objectLiteralExpr]);
376+
return factory.createDecorator(
377+
factory.createCallExpression(
378+
factory.createIdentifier(`${OPENAPI_NAMESPACE}.${ApiResponse.name}`),
379+
undefined,
380+
apiResponseDecoratorArguments
381+
)
382+
);
383+
});
384+
}
385+
305386
createDecoratorObjectLiteralExpr(
306387
factory: ts.NodeFactory,
307388
node: ts.MethodDeclaration,

‎test/plugin/fixtures/app.controller.ts

+11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export class AppController {
1515
* create a Cat
1616
*
1717
* @remarks Creating a test cat
18+
*
19+
* @throws {500} Something is wrong.
20+
* @throws {400} Bad Request.
21+
* @throws {400} Missing parameters.
1822
*
1923
* @returns {Promise<Cat>}
2024
* @memberof AppController
@@ -101,6 +105,10 @@ let AppController = exports.AppController = class AppController {
101105
*
102106
* @remarks Creating a test cat
103107
*
108+
* @throws {500} Something is wrong.
109+
* @throws {400} Bad Request.
110+
* @throws {400} Missing parameters.
111+
*
104112
* @returns {Promise<Cat>}
105113
* @memberof AppController
106114
*/
@@ -149,6 +157,9 @@ let AppController = exports.AppController = class AppController {
149157
};
150158
__decorate([
151159
openapi.ApiOperation({ summary: \"create a Cat\", description: \"Creating a test cat\" }),
160+
openapi.ApiResponse({ status: 500, description: "Something is wrong." }),
161+
openapi.ApiResponse({ status: 400, description: "Bad Request." }),
162+
openapi.ApiResponse({ status: 400, description: "Missing parameters." }),
152163
(0, common_1.Post)(),
153164
openapi.ApiResponse({ status: 201, type: Cat })
154165
], AppController.prototype, \"create\", null);

0 commit comments

Comments
 (0)
Please sign in to comment.