Repository: lukeautry/tsoa (ts)

Issue:
@Deprecated decorator for controller endpoints
It would be nice to have a `@Deprecated` decorator to be able to update your swagger documentation to display endpoints that are not being used/maintained any more.

Right now you can accomplish this updating the `tsoa.json` file with the endpoints you want to deprecate.

Pull request:
465 deprecated decorator
## Background
I think it would be nice to have a decorator to mark deprecated endpoints

## Changes done
- Created new deprecated decorator function
- Updated methodGenerator
- Updated unit tests
- Updated documentation

**Closing issues**

Closes #465

Patch:
diff --git a/README.MD b/README.MD
index c8a46d9b..f1a20c6b 100644
--- a/README.MD
+++ b/README.MD
@@ -34,6 +34,7 @@ Pronounced so·uh
     - [Security](#security)
     - [Tags](#tags)
     - [OperationId](#operationid)
+    - [Deprecated](#deprecated)
   * [Command Line Interface](#command-line-interface)
     - [Swagger spec generation](#swagger-spec-generation)
     - [Route generation](#route-generation)
@@ -776,6 +777,19 @@ public async find(): Promise<any> {
 }
 ```
 
+### Deprecated
+
+Declares this endpoint to be deprecated. Useful for when you are migrating endpoints and wants to keep a outdated
+version live until all consumers migrate to use the new endpoint version. 
+
+```ts
+@Get()
+@Deprecated()
+public async find(): Promise<any> {
+
+}
+```
+
 ## Command Line Interface
 
 For information on the configuration object (tsoa.json), check out the following:
diff --git a/src/decorators/deprecated.ts b/src/decorators/deprecated.ts
new file mode 100644
index 00000000..fba9ecc8
--- /dev/null
+++ b/src/decorators/deprecated.ts
@@ -0,0 +1,8 @@
+/**
+ * used to show a method as deprecated on swagger documentation
+ */
+export function Deprecated(): Function {
+  return () => {
+    return;
+  };
+}
diff --git a/src/metadataGeneration/methodGenerator.ts b/src/metadataGeneration/methodGenerator.ts
index 30efc29c..ca79450b 100644
--- a/src/metadataGeneration/methodGenerator.ts
+++ b/src/metadataGeneration/methodGenerator.ts
@@ -1,6 +1,6 @@
 import * as ts from 'typescript';
 import { getDecorators } from './../utils/decoratorUtils';
-import { getJSDocComment, getJSDocDescription, isExistJSDocTag } from './../utils/jsDocUtils';
+import {getJSDocComment, getJSDocDescription, isExistJSDocTag} from './../utils/jsDocUtils';
 import { GenerateMetadataError } from './exceptions';
 import { getInitializerValue } from './initializer-value';
 import { MetadataGenerator } from './metadataGenerator';
@@ -38,7 +38,7 @@ export class MethodGenerator {
     responses.push(this.getMethodSuccessResponse(type));
 
     return {
-      deprecated: isExistJSDocTag(this.node, tag => tag.tagName.text === 'deprecated'),
+      deprecated: this.getIsDeprecated(),
       description: getJSDocDescription(this.node),
       isHidden: this.getIsHidden(),
       method: this.method,
@@ -200,6 +200,21 @@ export class MethodGenerator {
     return example;
   }
 
+  private getIsDeprecated() {
+    if (isExistJSDocTag(this.node, tag => tag.tagName.text === 'deprecated')) {
+      return true;
+    }
+    const depDecorators = this.getDecoratorsByIdentifier(this.node, 'Deprecated');
+    if (!depDecorators || !depDecorators.length) {
+      return false;
+    }
+    if (depDecorators.length > 1) {
+      throw new GenerateMetadataError(`Only one Deprecated decorator allowed in '${this.getCurrentLocation}' method.`);
+    }
+
+    return true;
+  }
+
   private getOperationId() {
     const opDecorators = this.getDecoratorsByIdentifier(this.node, 'OperationId');
     if (!opDecorators || !opDecorators.length) {


Test patch:
diff --git a/tests/fixtures/controllers/deprecatedController.ts b/tests/fixtures/controllers/deprecatedController.ts
new file mode 100644
index 00000000..b627ac85
--- /dev/null
+++ b/tests/fixtures/controllers/deprecatedController.ts
@@ -0,0 +1,18 @@
+import { Controller, Get, Route } from '../../../src';
+import { TestModel } from '../../fixtures/testModel';
+import { ModelService } from '../services/modelService';
+import {Deprecated} from "../../../src/decorators/deprecated";
+
+@Route('Controller')
+export class DeprecatedController extends Controller {
+  @Get('normalGetMethod')
+  public async normalGetMethod(): Promise<TestModel> {
+    return Promise.resolve(new ModelService().getModel());
+  }
+
+  @Get('deprecatedGetMethod')
+  @Deprecated()
+  public async deprecatedGetMethod(): Promise<TestModel> {
+    return Promise.resolve(new ModelService().getModel());
+  }
+}
diff --git a/tests/unit/swagger/definitionsGeneration/metadata.spec.ts b/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
index cbdc87f6..dd9d083c 100644
--- a/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
@@ -502,4 +502,31 @@ describe('Metadata generation', () => {
       expect(method.isHidden).to.equal(true);
     });
   });
+
+  describe('DeprecatedMethodGenerator', () => {
+    const parameterMetadata = new MetadataGenerator('./tests/fixtures/controllers/deprecatedController.ts').Generate();
+    const controller = parameterMetadata.controllers[0];
+
+    it('should generate normal method', () => {
+      const method = controller.methods.find(m => m.name === 'normalGetMethod');
+      if (!method) {
+        throw new Error('Method normalGetMethod not defined!');
+      }
+
+      expect(method.method).to.equal('get');
+      expect(method.path).to.equal('normalGetMethod');
+      expect(method.deprecated).to.equal(false);
+    });
+
+    it('should generate deprecated method', () => {
+      const method = controller.methods.find(m => m.name === 'deprecatedGetMethod');
+      if (!method) {
+        throw new Error('Method deprecatedGetMethod not defined!');
+      }
+
+      expect(method.method).to.equal('get');
+      expect(method.path).to.equal('deprecatedGetMethod');
+      expect(method.deprecated).to.equal(true);
+    });
+  });
 });