Skip to content

Commit d9e0454

Browse files
committedFeb 13, 2024
feat: add error response decorator
1 parent feca721 commit d9e0454

File tree

2 files changed

+124
-5
lines changed

2 files changed

+124
-5
lines changed
 

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

+29
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,35 @@ export function getTsDocTagsOfNode(node: Node, typeChecker: TypeChecker) {
228228
return tagResults;
229229
}
230230

231+
export function getTsDocReturnsOrErrorOfNode(node: Node) {
232+
const tsdocParser: TSDocParser = new TSDocParser();
233+
const parserContext: ParserContext = tsdocParser.parseString(
234+
node.getFullText()
235+
);
236+
const docComment: DocComment = parserContext.docComment;
237+
238+
const tagResults = [];
239+
const introspectTsDocTags = (docComment: DocComment) => {
240+
const blocks = docComment.customBlocks.filter((block) =>
241+
['@throws', '@returns'].includes(block.blockTag.tagName)
242+
);
243+
244+
blocks.forEach((block) => {
245+
try {
246+
const docValue = renderDocNode(block.content).split('\n')[0].trim();
247+
const regex = /{(\d+)} (.*)/;
248+
const match = docValue.match(regex);
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

+95-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getDecoratorArguments,
1010
getDecoratorName,
1111
getMainCommentOfNode,
12+
getTsDocReturnsOrErrorOfNode,
1213
getTsDocTagsOfNode
1314
} from '../utils/ast-utils';
1415
import {
@@ -139,14 +140,34 @@ 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

145-
const existingDecorators = removeExistingApiOperationDecorator
146-
? decorators.filter(
147-
(item) => getDecoratorName(item) !== ApiOperation.name
148-
)
149-
: decorators;
157+
const removeExistingApiResponseDecorator =
158+
apiResponseDecoratorsArray.length > 0;
159+
160+
let existingDecorators = decorators;
161+
if (
162+
removeExistingApiOperationDecorator ||
163+
removeExistingApiResponseDecorator
164+
) {
165+
existingDecorators = decorators.filter(
166+
(item) =>
167+
getDecoratorName(item) !== ApiOperation.name &&
168+
getDecoratorName(item) !== ApiResponse.name
169+
);
170+
}
150171

151172
const modifiers = ts.getModifiers(compilerNode) ?? [];
152173
const objectLiteralExpr = this.createDecoratorObjectLiteralExpr(
@@ -160,6 +181,7 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
160181
);
161182
const updatedDecorators = [
162183
...apiOperationDecoratorsArray,
184+
...apiResponseDecoratorsArray,
163185
...existingDecorators,
164186
factory.createDecorator(
165187
factory.createCallExpression(
@@ -302,6 +324,74 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
302324
}
303325
}
304326

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

0 commit comments

Comments
 (0)
Please sign in to comment.