From d9e04544c3e1e280414de72a9dc0ae27977fb9b9 Mon Sep 17 00:00:00 2001 From: Tianning Li Date: Mon, 12 Feb 2024 16:05:53 -0800 Subject: [PATCH 1/5] feat: add error response decorator --- lib/plugin/utils/ast-utils.ts | 29 +++++ .../visitors/controller-class.visitor.ts | 100 +++++++++++++++++- 2 files changed, 124 insertions(+), 5 deletions(-) diff --git a/lib/plugin/utils/ast-utils.ts b/lib/plugin/utils/ast-utils.ts index 5eeea754b..9387df043 100644 --- a/lib/plugin/utils/ast-utils.ts +++ b/lib/plugin/utils/ast-utils.ts @@ -228,6 +228,35 @@ export function getTsDocTagsOfNode(node: Node, typeChecker: TypeChecker) { return tagResults; } +export function getTsDocReturnsOrErrorOfNode(node: Node) { + const tsdocParser: TSDocParser = new TSDocParser(); + const parserContext: ParserContext = tsdocParser.parseString( + node.getFullText() + ); + const docComment: DocComment = parserContext.docComment; + + const tagResults = []; + const introspectTsDocTags = (docComment: DocComment) => { + const blocks = docComment.customBlocks.filter((block) => + ['@throws', '@returns'].includes(block.blockTag.tagName) + ); + + blocks.forEach((block) => { + try { + const docValue = renderDocNode(block.content).split('\n')[0].trim(); + const regex = /{(\d+)} (.*)/; + const match = docValue.match(regex); + tagResults.push({ + status: match[1], + description: `"${match[2]}"` + }); + } catch (err) {} + }); + }; + introspectTsDocTags(docComment); + return tagResults; +} + export function getDecoratorArguments(decorator: Decorator) { const callExpression = decorator.expression; return (callExpression && (callExpression as CallExpression).arguments) || []; diff --git a/lib/plugin/visitors/controller-class.visitor.ts b/lib/plugin/visitors/controller-class.visitor.ts index 68162c6e5..794707e7b 100644 --- a/lib/plugin/visitors/controller-class.visitor.ts +++ b/lib/plugin/visitors/controller-class.visitor.ts @@ -9,6 +9,7 @@ import { getDecoratorArguments, getDecoratorName, getMainCommentOfNode, + getTsDocReturnsOrErrorOfNode, getTsDocTagsOfNode } from '../utils/ast-utils'; import { @@ -139,14 +140,34 @@ export class ControllerClassVisitor extends AbstractFileVisitor { typeChecker, metadata ); + + const apiResponseDecoratorsArray = this.createApiResponseDecorator( + factory, + compilerNode, + decorators, + options, + sourceFile, + typeChecker, + metadata + ); + const removeExistingApiOperationDecorator = apiOperationDecoratorsArray.length > 0; - const existingDecorators = removeExistingApiOperationDecorator - ? decorators.filter( - (item) => getDecoratorName(item) !== ApiOperation.name - ) - : decorators; + const removeExistingApiResponseDecorator = + apiResponseDecoratorsArray.length > 0; + + let existingDecorators = decorators; + if ( + removeExistingApiOperationDecorator || + removeExistingApiResponseDecorator + ) { + existingDecorators = decorators.filter( + (item) => + getDecoratorName(item) !== ApiOperation.name && + getDecoratorName(item) !== ApiResponse.name + ); + } const modifiers = ts.getModifiers(compilerNode) ?? []; const objectLiteralExpr = this.createDecoratorObjectLiteralExpr( @@ -160,6 +181,7 @@ export class ControllerClassVisitor extends AbstractFileVisitor { ); const updatedDecorators = [ ...apiOperationDecoratorsArray, + ...apiResponseDecoratorsArray, ...existingDecorators, factory.createDecorator( factory.createCallExpression( @@ -302,6 +324,74 @@ export class ControllerClassVisitor extends AbstractFileVisitor { } } + createApiResponseDecorator( + factory: ts.NodeFactory, + node: ts.MethodDeclaration, + decorators: readonly ts.Decorator[], + options: PluginOptions, + sourceFile: ts.SourceFile, + typeChecker: ts.TypeChecker, + metadata: ClassMetadata + ) { + if (!options.introspectComments) { + return []; + } + const apiResponseDecorator = getDecoratorOrUndefinedByNames( + [ApiResponse.name], + decorators, + factory + ); + let apiResponseExistingProps: + | ts.NodeArray + | undefined = undefined; + + if (apiResponseDecorator && !options.readonly) { + const apiResponseExpr = head(getDecoratorArguments(apiResponseDecorator)); + if (apiResponseExpr) { + apiResponseExistingProps = + apiResponseExpr.properties as ts.NodeArray; + } + } + + const tags = getTsDocReturnsOrErrorOfNode(node); + if (!tags.length) { + return []; + } + + return tags.map((tag) => { + const properties = [ + ...(apiResponseExistingProps ?? factory.createNodeArray()) + ]; + properties.push( + factory.createPropertyAssignment( + 'status', + factory.createNumericLiteral(tag.status) + ) + ); + properties.push( + factory.createPropertyAssignment( + 'description', + factory.createNumericLiteral(tag.description) + ) + ); + const objectLiteralExpr = factory.createObjectLiteralExpression( + compact(properties) + ); + const methodKey = node.name.getText(); + metadata[methodKey] = objectLiteralExpr; + + const apiResponseDecoratorArguments: ts.NodeArray = + factory.createNodeArray([objectLiteralExpr]); + return factory.createDecorator( + factory.createCallExpression( + factory.createIdentifier(`${OPENAPI_NAMESPACE}.${ApiResponse.name}`), + undefined, + apiResponseDecoratorArguments + ) + ); + }); + } + createDecoratorObjectLiteralExpr( factory: ts.NodeFactory, node: ts.MethodDeclaration, From e3eed25d3631826b1a2dea0a171ef54e4a937d65 Mon Sep 17 00:00:00 2001 From: Tianning Li Date: Mon, 12 Feb 2024 16:09:54 -0800 Subject: [PATCH 2/5] feat: update test --- test/plugin/fixtures/app.controller.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/plugin/fixtures/app.controller.ts b/test/plugin/fixtures/app.controller.ts index e407a9db7..8b6c35836 100644 --- a/test/plugin/fixtures/app.controller.ts +++ b/test/plugin/fixtures/app.controller.ts @@ -11,6 +11,10 @@ export class AppController { * create a Cat * * @remarks Creating a test cat + * + * @throws {500} Something is wrong. + * @throws {400} Bad Request. + * @throws {400} Missing parameters. * * @returns {Promise} * @memberof AppController @@ -75,6 +79,10 @@ let AppController = exports.AppController = class AppController { * * @remarks Creating a test cat * + * @throws {500} Something is wrong. + * @throws {400} Bad Request. + * @throws {400} Missing parameters. + * * @returns {Promise} * @memberof AppController */ @@ -109,6 +117,9 @@ let AppController = exports.AppController = class AppController { }; __decorate([ openapi.ApiOperation({ summary: \"create a Cat\", description: \"Creating a test cat\" }), + openapi.ApiResponse({ status: 500, description: "Something is wrong." }), + openapi.ApiResponse({ status: 400, description: "Bad Request." }), + openapi.ApiResponse({ status: 400, description: "Missing parameters." }), (0, common_1.Post)(), openapi.ApiResponse({ status: 201, type: Cat }) ], AppController.prototype, \"create\", null); From 746ee888cdb8af326f4eefabfbb0419f1440df36 Mon Sep 17 00:00:00 2001 From: Tianning Li Date: Mon, 12 Feb 2024 16:14:37 -0800 Subject: [PATCH 3/5] feat: update function name --- lib/plugin/utils/ast-utils.ts | 11 ++++++----- lib/plugin/visitors/controller-class.visitor.ts | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/plugin/utils/ast-utils.ts b/lib/plugin/utils/ast-utils.ts index 9387df043..e0e6a64a8 100644 --- a/lib/plugin/utils/ast-utils.ts +++ b/lib/plugin/utils/ast-utils.ts @@ -228,7 +228,7 @@ export function getTsDocTagsOfNode(node: Node, typeChecker: TypeChecker) { return tagResults; } -export function getTsDocReturnsOrErrorOfNode(node: Node) { +export function getTsDocErrorsOfNode(node: Node) { const tsdocParser: TSDocParser = new TSDocParser(); const parserContext: ParserContext = tsdocParser.parseString( node.getFullText() @@ -236,16 +236,17 @@ export function getTsDocReturnsOrErrorOfNode(node: Node) { const docComment: DocComment = parserContext.docComment; const tagResults = []; + const errorParsingRegex = /{(\d+)} (.*)/; + const introspectTsDocTags = (docComment: DocComment) => { - const blocks = docComment.customBlocks.filter((block) => - ['@throws', '@returns'].includes(block.blockTag.tagName) + const blocks = docComment.customBlocks.filter( + (block) => block.blockTag.tagName === '@throws' ); blocks.forEach((block) => { try { const docValue = renderDocNode(block.content).split('\n')[0].trim(); - const regex = /{(\d+)} (.*)/; - const match = docValue.match(regex); + const match = docValue.match(errorParsingRegex); tagResults.push({ status: match[1], description: `"${match[2]}"` diff --git a/lib/plugin/visitors/controller-class.visitor.ts b/lib/plugin/visitors/controller-class.visitor.ts index 794707e7b..75b9742d2 100644 --- a/lib/plugin/visitors/controller-class.visitor.ts +++ b/lib/plugin/visitors/controller-class.visitor.ts @@ -9,7 +9,7 @@ import { getDecoratorArguments, getDecoratorName, getMainCommentOfNode, - getTsDocReturnsOrErrorOfNode, + getTsDocErrorsOfNode, getTsDocTagsOfNode } from '../utils/ast-utils'; import { @@ -353,7 +353,7 @@ export class ControllerClassVisitor extends AbstractFileVisitor { } } - const tags = getTsDocReturnsOrErrorOfNode(node); + const tags = getTsDocErrorsOfNode(node); if (!tags.length) { return []; } From 445886f1890a487abaf21ded65e2ed46d4626d55 Mon Sep 17 00:00:00 2001 From: Tianning Li Date: Mon, 12 Feb 2024 16:21:47 -0800 Subject: [PATCH 4/5] feat: remove removing response decorator --- .../visitors/controller-class.visitor.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/lib/plugin/visitors/controller-class.visitor.ts b/lib/plugin/visitors/controller-class.visitor.ts index 75b9742d2..4793bfd38 100644 --- a/lib/plugin/visitors/controller-class.visitor.ts +++ b/lib/plugin/visitors/controller-class.visitor.ts @@ -154,20 +154,11 @@ export class ControllerClassVisitor extends AbstractFileVisitor { const removeExistingApiOperationDecorator = apiOperationDecoratorsArray.length > 0; - const removeExistingApiResponseDecorator = - apiResponseDecoratorsArray.length > 0; - - let existingDecorators = decorators; - if ( - removeExistingApiOperationDecorator || - removeExistingApiResponseDecorator - ) { - existingDecorators = decorators.filter( - (item) => - getDecoratorName(item) !== ApiOperation.name && - getDecoratorName(item) !== ApiResponse.name - ); - } + const existingDecorators = removeExistingApiOperationDecorator + ? decorators.filter( + (item) => getDecoratorName(item) !== ApiOperation.name + ) + : decorators; const modifiers = ts.getModifiers(compilerNode) ?? []; const objectLiteralExpr = this.createDecoratorObjectLiteralExpr( From 220ba97505347da7a19b7442747abc466de7ae39 Mon Sep 17 00:00:00 2001 From: Tianning Li Date: Fri, 8 Mar 2024 16:27:21 -0800 Subject: [PATCH 5/5] Refactor --- lib/plugin/utils/ast-utils.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/plugin/utils/ast-utils.ts b/lib/plugin/utils/ast-utils.ts index e0e6a64a8..ef3ecc5f3 100644 --- a/lib/plugin/utils/ast-utils.ts +++ b/lib/plugin/utils/ast-utils.ts @@ -138,15 +138,18 @@ export function getDefaultTypeFormatFlags(enclosingNode: Node) { return formatFlags; } -export function getMainCommentOfNode( - node: Node, - sourceFile: SourceFile -): string { +export function getDocComment(node: Node): DocComment { const tsdocParser: TSDocParser = new TSDocParser(); const parserContext: ParserContext = tsdocParser.parseString( node.getFullText() ); - const docComment: DocComment = parserContext.docComment; + return parserContext.docComment; +} +export function getMainCommentOfNode( + node: Node, + sourceFile: SourceFile +): string { + const docComment = getDocComment(node); return renderDocNode(docComment.summarySection).trim(); } @@ -168,11 +171,7 @@ export function parseCommentDocValue(docValue: string, type: ts.Type) { } export function getTsDocTagsOfNode(node: Node, typeChecker: TypeChecker) { - const tsdocParser: TSDocParser = new TSDocParser(); - const parserContext: ParserContext = tsdocParser.parseString( - node.getFullText() - ); - const docComment: DocComment = parserContext.docComment; + const docComment = getDocComment(node); const tagDefinitions: { [key: string]: {