From 32de7f56800204e972a79655fb53517d6ce505d5 Mon Sep 17 00:00:00 2001 From: shimks Date: Thu, 26 Apr 2018 12:40:40 -0400 Subject: [PATCH] feat(rest): add type coercion for parseParams --- packages/rest/package.json | 3 +- packages/rest/src/parser.ts | 13 ++- .../coercion/coercion.acceptance.ts | 83 +++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 packages/rest/test/acceptance/coercion/coercion.acceptance.ts diff --git a/packages/rest/package.json b/packages/rest/package.json index 25295b57234b..5e4fbcc10a15 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -32,7 +32,8 @@ "http-errors": "^1.6.3", "js-yaml": "^3.11.0", "lodash": "^4.17.5", - "path-to-regexp": "^2.2.0" + "path-to-regexp": "^2.2.0", + "tiny-coerce": "^1.0.1" }, "devDependencies": { "@loopback/build": "^0.6.0", diff --git a/packages/rest/src/parser.ts b/packages/rest/src/parser.ts index 210c361ed2fc..b7451734140b 100644 --- a/packages/rest/src/parser.ts +++ b/packages/rest/src/parser.ts @@ -18,6 +18,8 @@ import { PathParameterValues, } from './internal-types'; import {ResolvedRoute} from './router/routing-table'; +const coerce = require('tiny-coerce'); +const debug = require('debug')('loopback:rest:parser'); type HttpError = HttpErrors.HttpError; // tslint:disable-next-line:no-any @@ -124,5 +126,14 @@ function buildOperationArguments( } } if (requestBodyIndex > -1) paramArgs.splice(requestBodyIndex, 0, body); - return paramArgs; + + debug('Coercing parameters', paramArgs); + + const coercedParamArgs: OperationArgs = []; + for (const arg of paramArgs) { + debug('Coercing parameter', arg); + coercedParamArgs.push(typeof arg === 'object' ? arg : coerce(arg)); + } + + return coercedParamArgs; } diff --git a/packages/rest/test/acceptance/coercion/coercion.acceptance.ts b/packages/rest/test/acceptance/coercion/coercion.acceptance.ts new file mode 100644 index 000000000000..8877c26bca55 --- /dev/null +++ b/packages/rest/test/acceptance/coercion/coercion.acceptance.ts @@ -0,0 +1,83 @@ +import {supertest, createClientForHandler, sinon} from '@loopback/testlab'; +import { + RestApplication, + RestServer, + get, + param, + post, + requestBody, +} from '../../..'; + +describe('Coercion', () => { + let app: RestApplication; + let server: RestServer; + let client: supertest.SuperTest; + + before(givenAnApplication); + before(givenAServer); + before(givenAClient); + + after(async () => { + await app.stop(); + }); + + class MyController { + @get('/create-number/{num}') + getNumber(@param.path.number('num') num: number) { + return num; + } + + @get('/create-boolean') + getBoolean(@param.query.boolean('bool') bool: boolean) { + return bool; + } + + @post('/create-object/') + createObject(@requestBody() obj: object) {} + } + + it('coerces a number', async () => { + const spy = sinon.spy(MyController.prototype, 'getNumber'); + await client.get('/create-number/13').expect(200); + sinon.assert.calledWithExactly(spy, 13); + sinon.assert.neverCalledWith(spy, '13'); + spy.restore(); + }); + + it('coerces "false" into a boolean', async () => { + const spy = sinon.spy(MyController.prototype, 'getBoolean'); + await client.get('/create-boolean?bool=false').expect(200); + sinon.assert.calledWithExactly(spy, false); + sinon.assert.neverCalledWith(spy, 'false'); + spy.restore(); + }); + + it('coerces "true" into a boolean', async () => { + const spy = sinon.spy(MyController.prototype, 'getBoolean'); + await client.get('/create-boolean?bool=true').expect(200); + sinon.assert.calledWithExactly(spy, true); + sinon.assert.neverCalledWith(spy, 'true'); + spy.restore(); + }); + + it('works with requestBody', async () => { + await client + .post('/create-object') + .send({foo: 'bar'}) + .expect(200); + }); + + async function givenAnApplication() { + app = new RestApplication(); + app.controller(MyController); + await app.start(); + } + + async function givenAServer() { + server = await app.getServer(RestServer); + } + + async function givenAClient() { + client = await createClientForHandler(server.requestHandler); + } +});