From 4e2dc0f11a6952a0ba39e634a102f6005c48aa0d Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 30 Jan 2018 21:05:34 -0800 Subject: [PATCH] feat(rest): add basic HTTP parameter deserialization --- packages/rest/src/deserializer.ts | 114 ++++++++++ packages/rest/src/index.ts | 1 + packages/rest/src/parser.ts | 13 +- packages/rest/test/unit/deserializer.test.ts | 225 +++++++++++++++++++ 4 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 packages/rest/src/deserializer.ts create mode 100644 packages/rest/test/unit/deserializer.test.ts diff --git a/packages/rest/src/deserializer.ts b/packages/rest/src/deserializer.ts new file mode 100644 index 000000000000..82c93c599d79 --- /dev/null +++ b/packages/rest/src/deserializer.ts @@ -0,0 +1,114 @@ +// Copyright IBM Corp. 2017,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 * as assert from 'assert'; +import {ParameterObject} from '@loopback/openapi-spec'; +/** + * Simple deserializers for HTTP parameters + * @param val The value of the corresponding parameter type or a string + * @param param The Swagger parameter object + */ +// tslint:disable-next-line:no-any +export function deserialize(val: any, param: ParameterObject): any { + if (param.in === 'body') { + param = getBodyDescriptor(param); + } + if (val == null) { + if (param.required) { + throw new Error( + `Value is not provided for required parameter ${param.name}`, + ); + } + return val; + } + const type = param.type; + switch (type) { + case 'string': + if (typeof val === 'string') { + if (param.format === 'date' || param.format === 'date-time') { + return new Date(val); + } + return val; + } + throw new Error( + `Invalid value ${val} for parameter ${param.name}: ${type}`, + ); + case 'number': + case 'integer': + let num: number = NaN; + if (typeof val === 'string') { + num = Number(val); + } else if (typeof val === 'number') { + num = val; + } + if (isNaN(num)) { + throw new Error( + `Invalid value ${val} for parameter ${param.name}: ${type}`, + ); + } + if (type === 'integer' && !Number.isInteger(num)) { + throw new Error( + `Invalid value ${val} for parameter ${param.name}: ${type}`, + ); + } + return num; + case 'boolean': + if (typeof val === 'boolean') return val; + if (val === 'false') return false; + else if (val === 'true') return true; + throw new Error( + `Invalid value ${val} for parameter ${param.name}: ${type}`, + ); + case 'array': + let items = val; + if (typeof val === 'string') { + switch (param.collectionFormat) { + case 'ssv': // space separated values foo bar. + items = val.split(' '); + break; + case 'tsv': // tab separated values foo\tbar. + items = val.split('\t'); + break; + case 'pipes': // pipe separated values foo|bar. + items = val.split('|'); + break; + case 'csv': // comma separated values foo,bar. + default: + items = val.split(','); + } + } + if (Array.isArray(items)) { + return items.map(i => deserialize(i, getItemDescriptor(param))); + } + throw new Error( + `Invalid value ${val} for parameter ${param.name}: ${type}`, + ); + } + return val; +} + +/** + * Get the body descriptor + * @param param + */ +function getBodyDescriptor(param: ParameterObject): ParameterObject { + assert(param.in === 'body' && param.schema, 'Parameter location is not body'); + return Object.assign( + {in: param.in, name: param.name, description: param.description}, + param.schema, + ); +} + +/** + * Get the array item descriptor + * @param param + */ +function getItemDescriptor(param: ParameterObject): ParameterObject { + assert(param.type === 'array' && param.items, 'Parameter type is not array'); + return Object.assign( + {in: param.in, name: param.name, description: param.description}, + param.items, + ); +} diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index d730f186c957..62b542cd512e 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -22,6 +22,7 @@ export * from './providers'; import * as HttpErrors from 'http-errors'; export * from './parser'; +export * from './deserializer'; export {writeResultToResponse} from './writer'; diff --git a/packages/rest/src/parser.ts b/packages/rest/src/parser.ts index 95d5e0b8f21f..d0847fe35dea 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'; +import {deserialize} from './deserializer'; + type HttpError = HttpErrors.HttpError; // tslint:disable-next-line:no-any @@ -106,15 +108,20 @@ function buildOperationArguments( throw new Error('$ref parameters are not supported yet.'); } const spec = paramSpec as ParameterObject; + // tslint:disable-next-line:no-any + const addArg = (val: any) => paramArgs.push(deserialize(val, spec)); switch (spec.in) { case 'query': - paramArgs.push(request.query[spec.name]); + addArg(request.query[spec.name]); break; case 'path': - paramArgs.push(pathParams[spec.name]); + addArg(pathParams[spec.name]); break; case 'header': - paramArgs.push(request.headers[spec.name.toLowerCase()]); + addArg(request.headers[spec.name.toLowerCase()]); + break; + case 'cookie': + // addArg(request.cookies[spec.name]); break; // TODO(jannyhou) to support `cookie`, // see issue https://github.com/strongloop/loopback-next/issues/997 diff --git a/packages/rest/test/unit/deserializer.test.ts b/packages/rest/test/unit/deserializer.test.ts new file mode 100644 index 000000000000..cb69b48139f4 --- /dev/null +++ b/packages/rest/test/unit/deserializer.test.ts @@ -0,0 +1,225 @@ +// Copyright IBM Corp. 2017,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 {deserialize} from '../..'; +import {expect} from '@loopback/testlab'; +import {ParameterObject} from '@loopback/openapi-spec'; + +// tslint:disable:no-any +describe('deserializer', () => { + it('converts number parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'balance', + type: 'number', + }; + expectToDeserialize(param, [0, 1.5, '10', '2.5'], [0, 1.5, 10, 2.5]); + expectToDeserializeNullOrUndefined(param); + }); + + it('reports errors for invalid number parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'balance', + type: 'number', + }; + expectToFail( + param, + ['a', 'a1', 'true', true, false, {}, new Date()], + /Invalid value .* for parameter balance\: number/, + ); + }); + + it('converts integer parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'id', + type: 'integer', + }; + expectToDeserialize(param, [0, -1, '10', '-5'], [0, -1, 10, -5]); + }); + + it('reports erros for invalid integer parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'id', + type: 'integer', + }; + expectToFail( + param, + ['a', 'a1', 'true', {}, 1.5, '-2.5'], + /Invalid value .* for parameter id\: integer/, + ); + }); + + it('converts boolean parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'vip', + type: 'boolean', + }; + expectToDeserialize( + param, + [true, false, 'true', 'false'], + [true, false, true, false], + ); + expectToDeserializeNullOrUndefined(param); + }); + + it('reports errors for invalid boolean parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'vip', + type: 'boolean', + }; + expectToFail( + param, + ['a', 'a1', {}, 1.5, 0, 1, -10], + /Invalid value .* for parameter vip\: boolean/, + ); + }); + + it('converts string parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'name', + type: 'string', + }; + expectToDeserialize(param, ['', 'A'], ['', 'A']); + expectToDeserializeNullOrUndefined(param); + }); + + it('reports errors for invalid string parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'name', + type: 'string', + }; + expectToFail( + param, + [true, false, 0, -1, 2.5, {}, new Date()], + /Invalid value .* for parameter name\: string/, + ); + }); + + it('converts date parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'date', + type: 'string', + format: 'date', + }; + const date = new Date(); + expectToDeserialize(param, [date.toJSON()], [date]); + }); + + describe('string[]', () => { + it('converts csv format', () => { + const param: ParameterObject = { + in: 'query', + name: 'nums', + type: 'array', + collectionFormat: 'csv', + items: { + type: 'string', + }, + }; + expectToDeserialize( + param, + ['1,2,3', 'ab,c'], + [['1', '2', '3'], ['ab', 'c']], + ); + }); + + it('converts ssv format', () => { + const param: ParameterObject = { + in: 'query', + name: 'nums', + type: 'array', + collectionFormat: 'ssv', + items: { + type: 'string', + }, + }; + expectToDeserialize( + param, + ['1 2 3', 'ab c'], + [['1', '2', '3'], ['ab', 'c']], + ); + }); + + it('converts pipes format', () => { + const param: ParameterObject = { + in: 'query', + name: 'nums', + type: 'array', + collectionFormat: 'pipes', + items: { + type: 'string', + }, + }; + expectToDeserialize( + param, + ['1|2|3', 'ab|c'], + [['1', '2', '3'], ['ab', 'c']], + ); + }); + + it('converts tsv format', () => { + const param: ParameterObject = { + in: 'query', + name: 'nums', + type: 'array', + collectionFormat: 'tsv', + items: { + type: 'string', + }, + }; + expectToDeserialize( + param, + ['1\t2\t3', 'ab\tc'], + [['1', '2', '3'], ['ab', 'c']], + ); + }); + }); + + describe('number[]', () => { + it('converts csv format', () => { + const param: ParameterObject = { + in: 'query', + name: 'nums', + type: 'array', + collectionFormat: 'csv', + items: { + type: 'number', + }, + }; + expectToDeserialize(param, ['1,2,3', '-10,2.5'], [[1, 2, 3], [-10, 2.5]]); + }); + }); + + function expectToDeserialize( + param: ParameterObject, + source: any[], + target: any[], + ) { + expect(source.map(i => deserialize(i, param))).to.eql(target); + } + + function expectToDeserializeNullOrUndefined(param: ParameterObject) { + expect(deserialize(null, param)).to.be.null(); + expect(deserialize(undefined, param)).to.be.undefined(); + } + + function expectToFail( + param: ParameterObject, + source: any[], + reason: string | RegExp, + ) { + for (const i of source) { + expect(() => deserialize(i, param)).to.throw(reason); + } + } +});