Repository: lukeautry/tsoa
Pull request #286 (2018-09-21 09:22:43)

Added missing HTTP method HEAD


Patch:
diff --git a/src/decorators/methods.ts b/src/decorators/methods.ts
index c71d627e..859f1959 100644
--- a/src/decorators/methods.ts
+++ b/src/decorators/methods.ts
@@ -17,3 +17,7 @@ export function Patch(value?: string): Function {
 export function Delete(value?: string): Function {
   return () => { return; };
 }
+
+export function Head(value?: string): Function {
+  return () => { return; };
+}
diff --git a/src/metadataGeneration/methodGenerator.ts b/src/metadataGeneration/methodGenerator.ts
index c5b31b30..073050ec 100644
--- a/src/metadataGeneration/methodGenerator.ts
+++ b/src/metadataGeneration/methodGenerator.ts
@@ -9,7 +9,7 @@ import { getSecurities } from './security';
 import { Tsoa } from './tsoa';
 
 export class MethodGenerator {
-  private method: 'get' | 'post' | 'put' | 'patch' | 'delete';
+  private method: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
   private path: string;
 
   constructor(
@@ -191,7 +191,7 @@ export class MethodGenerator {
   }
 
   private supportsPathMethod(method: string) {
-    return ['get', 'post', 'put', 'patch', 'delete'].some((m) => m === method.toLowerCase());
+    return ['get', 'post', 'put', 'patch', 'delete', 'head'].some((m) => m === method.toLowerCase());
   }
 
   private getExamplesValue(argument: any) {
diff --git a/src/metadataGeneration/tsoa.ts b/src/metadataGeneration/tsoa.ts
index 0457c566..a87e702b 100644
--- a/src/metadataGeneration/tsoa.ts
+++ b/src/metadataGeneration/tsoa.ts
@@ -14,7 +14,7 @@ export namespace Tsoa {
   export interface Method {
     deprecated?: boolean;
     description?: string;
-    method: 'get' | 'post' | 'put' | 'delete' | 'options' | 'head' | 'patch';
+    method: 'get' | 'post' | 'put' | 'delete' | 'options' | 'head' | 'patch' | 'head';
     name: string;
     parameters: Parameter[];
     path: string;


Test patch:
diff --git a/tests/fixtures/controllers/headController.ts b/tests/fixtures/controllers/headController.ts
new file mode 100644
index 00000000..34002814
--- /dev/null
+++ b/tests/fixtures/controllers/headController.ts
@@ -0,0 +1,27 @@
+import {
+  Head, Query, Route,
+} from '../../../src';
+
+@Route('HeadTest')
+export class HeadTestController {
+  @Head()
+  public async resourceExists(): Promise<void> {
+    return;
+  }
+
+  @Head('Current')
+  public async resourceExistsCurrent(): Promise<void> {
+    return;
+  }
+
+  @Head('{numberPathParam}/{booleanPathParam}/{stringPathParam}')
+  public async resourceExistsByParams(
+    numberPathParam: number,
+    stringPathParam: string,
+    booleanPathParam: boolean,
+    @Query() booleanParam: boolean,
+    @Query() stringParam: string,
+    @Query() numberParam: number): Promise<void> {
+    return;
+  }
+}
diff --git a/tests/fixtures/custom/routes.ts b/tests/fixtures/custom/routes.ts
index fa959344..3c777423 100644
--- a/tests/fixtures/custom/routes.ts
+++ b/tests/fixtures/custom/routes.ts
@@ -2,6 +2,7 @@
 import { Controller, ValidateParam, FieldErrors, ValidateError, TsoaRoute } from '../../../src';
 import { DeleteTestController } from './../controllers/deleteController';
 import { GetTestController } from './../controllers/getController';
+import { HeadTestController } from './../controllers/headController';
 import { PatchTestController } from './../controllers/patchController';
 import { PostTestController } from './../controllers/postController';
 import { PutTestController } from './../controllers/putController';
@@ -564,6 +565,66 @@ export function RegisterRoutes(app: any) {
       const promise=controller.getGenericPrimitiveArray.apply(controller, validatedArgs);
       promiseHandler(controller, promise, response, next);
     });
+  app.head('/v1/HeadTest',
+    function(request: any, response: any, next: any) {
+      const args={
+      };
+
+      let validatedArgs: any[]=[];
+      try {
+        validatedArgs=getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller=new HeadTestController();
+
+
+      const promise=controller.resourceExists.apply(controller, validatedArgs);
+      promiseHandler(controller, promise, response, next);
+    });
+  app.head('/v1/HeadTest/Current',
+    function(request: any, response: any, next: any) {
+      const args={
+      };
+
+      let validatedArgs: any[]=[];
+      try {
+        validatedArgs=getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller=new HeadTestController();
+
+
+      const promise=controller.resourceExistsCurrent.apply(controller, validatedArgs);
+      promiseHandler(controller, promise, response, next);
+    });
+  app.head('/v1/HeadTest/:numberPathParam/:booleanPathParam/:stringPathParam',
+    function(request: any, response: any, next: any) {
+      const args={
+        numberPathParam: { "in": "path", "name": "numberPathParam", "required": true, "dataType": "double" },
+        stringPathParam: { "in": "path", "name": "stringPathParam", "required": true, "dataType": "string" },
+        booleanPathParam: { "in": "path", "name": "booleanPathParam", "required": true, "dataType": "boolean" },
+        booleanParam: { "in": "query", "name": "booleanParam", "required": true, "dataType": "boolean" },
+        stringParam: { "in": "query", "name": "stringParam", "required": true, "dataType": "string" },
+        numberParam: { "in": "query", "name": "numberParam", "required": true, "dataType": "double" },
+      };
+
+      let validatedArgs: any[]=[];
+      try {
+        validatedArgs=getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller=new HeadTestController();
+
+
+      const promise=controller.resourceExistsByParams.apply(controller, validatedArgs);
+      promiseHandler(controller, promise, response, next);
+    });
   app.patch('/v1/PatchTest',
     function(request: any, response: any, next: any) {
       const args={
diff --git a/tests/fixtures/custom/server.ts b/tests/fixtures/custom/server.ts
index aec1d383..ec1dd5fb 100644
--- a/tests/fixtures/custom/server.ts
+++ b/tests/fixtures/custom/server.ts
@@ -3,6 +3,7 @@ import * as express from 'express';
 import * as methodOverride from 'method-override';
 import '../controllers/deleteController';
 import '../controllers/getController';
+import '../controllers/headController';
 import '../controllers/patchController';
 import '../controllers/postController';
 import '../controllers/putController';
diff --git a/tests/fixtures/express/routes.ts b/tests/fixtures/express/routes.ts
index c39e1899..5f5b483b 100644
--- a/tests/fixtures/express/routes.ts
+++ b/tests/fixtures/express/routes.ts
@@ -3,6 +3,7 @@ import { Controller, ValidateParam, FieldErrors, ValidateError, TsoaRoute } from
 import { RootController } from './../controllers/rootController';
 import { DeleteTestController } from './../controllers/deleteController';
 import { GetTestController } from './../controllers/getController';
+import { HeadTestController } from './../controllers/headController';
 import { PatchTestController } from './../controllers/patchController';
 import { PostTestController } from './../controllers/postController';
 import { PutTestController } from './../controllers/putController';
@@ -647,6 +648,66 @@ export function RegisterRoutes(app: any) {
       const promise=controller.getGenericPrimitiveArray.apply(controller, validatedArgs);
       promiseHandler(controller, promise, response, next);
     });
+  app.head('/v1/HeadTest',
+    function(request: any, response: any, next: any) {
+      const args={
+      };
+
+      let validatedArgs: any[]=[];
+      try {
+        validatedArgs=getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller=new HeadTestController();
+
+
+      const promise=controller.resourceExists.apply(controller, validatedArgs);
+      promiseHandler(controller, promise, response, next);
+    });
+  app.head('/v1/HeadTest/Current',
+    function(request: any, response: any, next: any) {
+      const args={
+      };
+
+      let validatedArgs: any[]=[];
+      try {
+        validatedArgs=getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller=new HeadTestController();
+
+
+      const promise=controller.resourceExistsCurrent.apply(controller, validatedArgs);
+      promiseHandler(controller, promise, response, next);
+    });
+  app.head('/v1/HeadTest/:numberPathParam/:booleanPathParam/:stringPathParam',
+    function(request: any, response: any, next: any) {
+      const args={
+        numberPathParam: { "in": "path", "name": "numberPathParam", "required": true, "dataType": "double" },
+        stringPathParam: { "in": "path", "name": "stringPathParam", "required": true, "dataType": "string" },
+        booleanPathParam: { "in": "path", "name": "booleanPathParam", "required": true, "dataType": "boolean" },
+        booleanParam: { "in": "query", "name": "booleanParam", "required": true, "dataType": "boolean" },
+        stringParam: { "in": "query", "name": "stringParam", "required": true, "dataType": "string" },
+        numberParam: { "in": "query", "name": "numberParam", "required": true, "dataType": "double" },
+      };
+
+      let validatedArgs: any[]=[];
+      try {
+        validatedArgs=getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller=new HeadTestController();
+
+
+      const promise=controller.resourceExistsByParams.apply(controller, validatedArgs);
+      promiseHandler(controller, promise, response, next);
+    });
   app.patch('/v1/PatchTest',
     function(request: any, response: any, next: any) {
       const args={
diff --git a/tests/fixtures/express/server.ts b/tests/fixtures/express/server.ts
index b5b8809c..c66788f0 100644
--- a/tests/fixtures/express/server.ts
+++ b/tests/fixtures/express/server.ts
@@ -5,6 +5,7 @@ import '../controllers/rootController';
 
 import '../controllers/deleteController';
 import '../controllers/getController';
+import '../controllers/headController';
 import '../controllers/patchController';
 import '../controllers/postController';
 import '../controllers/putController';
diff --git a/tests/fixtures/koa/routes.ts b/tests/fixtures/koa/routes.ts
index da7b435a..2d7005ac 100644
--- a/tests/fixtures/koa/routes.ts
+++ b/tests/fixtures/koa/routes.ts
@@ -3,6 +3,7 @@ import { Controller, ValidateParam, FieldErrors, ValidateError, TsoaRoute } from
 import { RootController } from './../controllers/rootController';
 import { DeleteTestController } from './../controllers/deleteController';
 import { GetTestController } from './../controllers/getController';
+import { HeadTestController } from './../controllers/headController';
 import { PatchTestController } from './../controllers/patchController';
 import { PostTestController } from './../controllers/postController';
 import { PutTestController } from './../controllers/putController';
@@ -669,6 +670,69 @@ export function RegisterRoutes(router: any) {
       const promise=controller.getGenericPrimitiveArray.apply(controller, validatedArgs);
       return promiseHandler(controller, promise, context, next);
     });
+  router.head('/v1/HeadTest',
+    async (context, next) => {
+      const args={
+      };
+
+      let validatedArgs: any[]=[];
+      try {
+        validatedArgs=getValidatedArgs(args, context);
+      } catch (error) {
+        context.status=error.status;
+        context.throw(error.status, JSON.stringify({ fields: error.fields }));
+        return next();
+      }
+
+      const controller=new HeadTestController();
+
+      const promise=controller.resourceExists.apply(controller, validatedArgs);
+      return promiseHandler(controller, promise, context, next);
+    });
+  router.head('/v1/HeadTest/Current',
+    async (context, next) => {
+      const args={
+      };
+
+      let validatedArgs: any[]=[];
+      try {
+        validatedArgs=getValidatedArgs(args, context);
+      } catch (error) {
+        context.status=error.status;
+        context.throw(error.status, JSON.stringify({ fields: error.fields }));
+        return next();
+      }
+
+      const controller=new HeadTestController();
+
+      const promise=controller.resourceExistsCurrent.apply(controller, validatedArgs);
+      return promiseHandler(controller, promise, context, next);
+    });
+  router.head('/v1/HeadTest/:numberPathParam/:booleanPathParam/:stringPathParam',
+    async (context, next) => {
+      const args={
+        numberPathParam: { "in": "path", "name": "numberPathParam", "required": true, "dataType": "double" },
+        stringPathParam: { "in": "path", "name": "stringPathParam", "required": true, "dataType": "string" },
+        booleanPathParam: { "in": "path", "name": "booleanPathParam", "required": true, "dataType": "boolean" },
+        booleanParam: { "in": "query", "name": "booleanParam", "required": true, "dataType": "boolean" },
+        stringParam: { "in": "query", "name": "stringParam", "required": true, "dataType": "string" },
+        numberParam: { "in": "query", "name": "numberParam", "required": true, "dataType": "double" },
+      };
+
+      let validatedArgs: any[]=[];
+      try {
+        validatedArgs=getValidatedArgs(args, context);
+      } catch (error) {
+        context.status=error.status;
+        context.throw(error.status, JSON.stringify({ fields: error.fields }));
+        return next();
+      }
+
+      const controller=new HeadTestController();
+
+      const promise=controller.resourceExistsByParams.apply(controller, validatedArgs);
+      return promiseHandler(controller, promise, context, next);
+    });
   router.patch('/v1/PatchTest',
     async (context, next) => {
       const args={
diff --git a/tests/fixtures/koa/server.ts b/tests/fixtures/koa/server.ts
index 80e32d92..5f083062 100644
--- a/tests/fixtures/koa/server.ts
+++ b/tests/fixtures/koa/server.ts
@@ -4,6 +4,7 @@ import '../controllers/rootController';
 
 import '../controllers/deleteController';
 import '../controllers/getController';
+import '../controllers/headController';
 import '../controllers/patchController';
 import '../controllers/postController';
 import '../controllers/putController';
diff --git a/tests/unit/swagger/pathGeneration/headRoutes.spec.ts b/tests/unit/swagger/pathGeneration/headRoutes.spec.ts
new file mode 100644
index 00000000..c3d543f9
--- /dev/null
+++ b/tests/unit/swagger/pathGeneration/headRoutes.spec.ts
@@ -0,0 +1,51 @@
+import 'mocha';
+import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
+import { SpecGenerator } from '../../../../src/swagger/specGenerator';
+import { getDefaultOptions } from '../../../fixtures/defaultOptions';
+import { VerifyPathableParameter } from '../../utilities/verifyParameter';
+import { VerifyPath } from '../../utilities/verifyPath';
+
+describe('HEAD route generation', () => {
+  const metadata = new MetadataGenerator('./tests/fixtures/controllers/headController.ts').Generate();
+  const spec = new SpecGenerator(metadata, getDefaultOptions()).GetSpec();
+  const baseRoute = '/HeadTest';
+
+  it('should generate a path for a HEAD route with no path argument', () => {
+    verifyPath(baseRoute, false, true);
+  });
+
+  it('should generate a path for a HEAD route with a path argument', () => {
+    const actionRoute = `${baseRoute}/Current`;
+    verifyPath(actionRoute, false, true);
+  });
+
+  it('should generate a parameter for path parameters', () => {
+    const actionRoute = `${baseRoute}/{numberPathParam}/{booleanPathParam}/{stringPathParam}`;
+    const parameters = getVerifiedParameters(actionRoute);
+
+    VerifyPathableParameter(parameters, 'booleanPathParam', 'boolean', 'path');
+    VerifyPathableParameter(parameters, 'numberPathParam', 'number', 'path', 'double');
+    VerifyPathableParameter(parameters, 'stringPathParam', 'string', 'path');
+  });
+
+  it('should generate a parameter for query parameters', () => {
+    const actionRoute = `${baseRoute}/{numberPathParam}/{booleanPathParam}/{stringPathParam}`;
+    const parameters = getVerifiedParameters(actionRoute);
+
+    VerifyPathableParameter(parameters, 'booleanParam', 'boolean', 'query');
+    VerifyPathableParameter(parameters, 'numberParam', 'number', 'query', 'double');
+    VerifyPathableParameter(parameters, 'stringParam', 'string', 'query');
+  });
+
+  function verifyPath(route: string, isCollection?: boolean, isNoContent?: boolean) {
+    return VerifyPath(spec, route, path => path.head, isCollection, isNoContent);
+  }
+
+  function getVerifiedParameters(actionRoute: string) {
+    const path = verifyPath(actionRoute, false, true);
+    if (!path.head) { throw new Error('No head operation.'); }
+    if (!path.head.parameters) { throw new Error('No operation parameters.'); }
+
+    return path.head.parameters as any;
+  }
+});