Skip to content

Commit

Permalink
feat: add type coercion
Browse files Browse the repository at this point in the history
  • Loading branch information
jannyHou committed Jun 1, 2018
1 parent 9241f5a commit 71987b0
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 16 deletions.
121 changes: 121 additions & 0 deletions packages/rest/src/coercion/coerce-parameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {
SchemaObject,
ReferenceObject,
isReferenceObject,
} from '@loopback/openapi-v3-types';

import * as HttpErrors from 'http-errors';

/**
* Coerce the http raw data to a JavaScript type data of a parameter
* according to its OpenAPI schema specification.
*
* @param data The raw data get from http request
* @param schema The parameter's schema defined in OpenAPI specification
*/
export function coerceParameter(
data: string,
schema?: SchemaObject | ReferenceObject,
) {
// ignore reference schema
if (!schema || isReferenceObject(schema)) return data;

let coercedResult;
coercedResult = data;

const OAIType = getOAIPrimitiveType(schema.type, schema.format);

switch (OAIType) {
case 'byte':
coercedResult = Buffer.from(data, 'base64');
break;
case 'date':
coercedResult = new Date(data);
break;
case 'float':
case 'double':
coercedResult = parseFloat(data);
break;
case 'number':
case 'long':
coercedResult = Number(data);
break;
case 'integer':
coercedResult = parseInt(data);
break;
case 'boolean':
coercedResult = isTrue(data) ? true : false;
case 'string':
case 'password':
// serizlize will be supported in next PR
case 'serialize':
break;
case 'unknownType':
default:
throw new HttpErrors.NotImplemented(
`Type ${schema.type} with format ${
schema.format
} is not a valid OpenAPI schema`,
);
}
return coercedResult;
}

/**
* A set of truthy values. A data in this set will be coerced to `true`,
* otherwise coerced to `false`.
*
* @param data The raw data get from http request
* @returns The corresponding coerced boolean type
*/

function isTrue(data: string): boolean {
const isTrueSet = ['true', '1', true, 1];
return isTrueSet.includes(data);
}

/**
* Return the corresponding OpenAPI data type given an OpenAPI schema
*
* @param type The type in an OpenAPI schema specification
* @param format The format in an OpenAPI schema specification
*/

function getOAIPrimitiveType(type?: string, format?: string) {
let OAIType: string = 'unknownType';
// serizlize will be supported in next PR
if (type === 'object' || type === 'array') OAIType = 'serialize';
if (type === 'string') {
switch (format) {
case 'byte':
OAIType = 'byte';
break;
case 'binary':
OAIType = 'binary';
break;
case 'date':
OAIType = 'date';
break;
case 'date-time':
OAIType = 'date-time';
break;
case 'password':
OAIType = 'password';
break;
default:
OAIType = 'string';
break;
}
}
if (type === 'boolean') OAIType = 'boolean';
if (type === 'number')
OAIType =
format === 'float' ? 'float' : format === 'double' ? 'double' : 'number';
if (type === 'integer') OAIType = format === 'int64' ? 'long' : 'integer';
return OAIType;
}
42 changes: 26 additions & 16 deletions packages/rest/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {REQUEST_BODY_INDEX} from '@loopback/openapi-v3';
import {promisify} from 'util';
import {OperationArgs, Request, PathParameterValues} from './types';
import {ResolvedRoute} from './router/routing-table';
import {coerceParameter} from './coercion/coerce-parameter';
type HttpError = HttpErrors.HttpError;

// tslint:disable-next-line:no-any
Expand Down Expand Up @@ -101,22 +102,31 @@ function buildOperationArguments(
throw new Error('$ref parameters are not supported yet.');
}
const spec = paramSpec as ParameterObject;
switch (spec.in) {
case 'query':
paramArgs.push(request.query[spec.name]);
break;
case 'path':
paramArgs.push(pathParams[spec.name]);
break;
case 'header':
paramArgs.push(request.headers[spec.name.toLowerCase()]);
break;
// TODO(jannyhou) to support `cookie`,
// see issue https://github.com/strongloop/loopback-next/issues/997
default:
throw new HttpErrors.NotImplemented(
'Parameters with "in: ' + spec.in + '" are not supported yet.',
);
const rawValue = getParamFromRequest();
const coercedValue = coerceParameter(rawValue, spec.schema);
paramArgs.push(coercedValue);

function getParamFromRequest() {
let result;
switch (spec.in) {
case 'query':
result = request.query[spec.name];
break;
case 'path':
result = pathParams[spec.name];
break;
case 'header':
// @jannyhou TBD: check edge cases
result = request.headers[spec.name.toLowerCase()];
break;
// TODO(jannyhou) to support `cookie`,
// see issue https://github.com/strongloop/loopback-next/issues/997
default:
throw new HttpErrors.NotImplemented(
'Parameters with "in: ' + spec.in + '" are not supported yet.',
);
}
return result;
}
}
if (requestBodyIndex > -1) paramArgs.splice(requestBodyIndex, 0, body);
Expand Down
78 changes: 78 additions & 0 deletions packages/rest/test/acceptance/coercion/coercion.acceptance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
supertest,
createClientForHandler,
sinon,
SinonSpy,
} from '@loopback/testlab';
import {RestApplication, RestServer, get, param} from '../../..';

describe('Coercion', () => {
let app: RestApplication;
let server: RestServer;
let client: supertest.SuperTest<supertest.Test>;

before(givenAnApplication);
before(givenAServer);
before(givenAClient);

after(async () => {
await app.stop();
});

class MyController {
@get('/create-number-from-path/{num}')
createNumberFromPath(@param.path.number('num') num: number) {
return num;
}

@get('/create-number-from-query')
createNumberFromQuery(@param.query.number('num') num: number) {
return num;
}

@get('/create-number-from-header')
createNumberFromHeader(@param.header.number('num') num: number) {
return num;
}
}

it('coerces parameter in path from string to number', async () => {
const spy = sinon.spy(MyController.prototype, 'createNumberFromPath');
await client.get('/create-number-from-path/100').expect(200);
assertCoercion(spy);
});

it('coerces parameter in header from string to number', async () => {
const spy = sinon.spy(MyController.prototype, 'createNumberFromHeader');
await client.get('/create-number-from-header').set({num: 100});
assertCoercion(spy);
});

it('coerces parameter in query from string to number', async () => {
const spy = sinon.spy(MyController.prototype, 'createNumberFromQuery');
await client
.get('/create-number-from-query')
.query({num: 100})
.expect(200);
assertCoercion(spy);
});

function assertCoercion(spy: SinonSpy) {
sinon.assert.calledWithExactly(spy, 100);
sinon.assert.neverCalledWith(spy, '100');
}

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);
}
});
18 changes: 18 additions & 0 deletions packages/rest/test/unit/coercion/paramStringToBoolean.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {testCoercion} from './utils';

describe('coerce param from string to boolean', () => {
it("value 'false' is coerced to false", async () => {
const caller = new Error().stack!.split(/\n/)[1];
await testCoercion<boolean>({type: 'boolean'}, 'false', false, caller);
});

it("value 'true' is coerced to true", async () => {
const caller = new Error().stack!.split(/\n/)[1];
await testCoercion<boolean>({type: 'boolean'}, 'true', true, caller);
});
});
20 changes: 20 additions & 0 deletions packages/rest/test/unit/coercion/paramStringToBuffer.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {testCoercion} from './utils';

describe('coerce param from string to buffer', () => {
it('base64', async () => {
const base64 = Buffer.from('Hello World').toString('base64');
const buffer = Buffer.from(base64, 'base64');
const caller = new Error().stack!.split(/\n/)[1];
await testCoercion<Buffer>(
{type: 'string', format: 'byte'},
base64,
buffer,
caller,
);
});
});
18 changes: 18 additions & 0 deletions packages/rest/test/unit/coercion/paramStringToDate.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {testCoercion} from './utils';

describe('coerce param from string to date', () => {
it('simple date', async () => {
const caller = new Error().stack!.split(/\n/)[1];
await testCoercion<Date>(
{type: 'string', format: 'date'},
'2015-03-01',
new Date('2015-03-01'),
caller,
);
});
});
48 changes: 48 additions & 0 deletions packages/rest/test/unit/coercion/paramStringToNumber.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {testCoercion} from './utils';

describe('coerce param from string to number', () => {
it('string to float', async () => {
const caller = new Error().stack!.split(/\n/)[1];
await testCoercion<number>(
{type: 'number', format: 'float'},
'3.333333',
3.333333,
caller,
);
});

it('string to double', async () => {
const caller = new Error().stack!.split(/\n/)[1];
await testCoercion<number>(
{type: 'number', format: 'double'},
'3.333333333',
3.333333333,
caller,
);
});

it('string to integer', async () => {
const caller = new Error().stack!.split(/\n/)[1];
await testCoercion<number>(
{type: 'integer', format: 'int32'},
'100',
100,
caller,
);
});

it('string to long', async () => {
const caller = new Error().stack!.split(/\n/)[1];
await testCoercion<number>(
{type: 'integer', format: 'int64'},
'9223372036854775807',
9223372036854775807,
caller,
);
});
});
Loading

0 comments on commit 71987b0

Please sign in to comment.