diff --git a/packages/rest/src/deserializer.ts b/packages/rest/src/deserializer.ts new file mode 100644 index 000000000000..b010ec26cb52 --- /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, isSchemaObject} from '@loopback/openapi-v3-types'; +/** + * 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 (val == null) { + if (param.required) { + throw new Error( + `Value is not provided for required parameter ${param.name}`, + ); + } + return val; + } + let type = 'string'; + let format = ''; + if (param.schema && isSchemaObject(param.schema)) { + type = param.schema.type || 'string'; + format = param.schema.format || ''; + } + + const style = param.style; + + if (style === 'matrix' || style === 'label') { + throw new Error(`Parameter ${param.name} style ${style} is not supported`); + } + + switch (type) { + case 'string': + if (typeof val === 'string') { + if (format === 'date' || format === 'date-time') { + return new Date(val); + } else if (format === 'byte') { + return Buffer.from(val, 'base64').toString('utf8'); + } + 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 (style) { + case 'spaceDelimited': // space separated values foo bar. + items = val.split(' '); + break; + case 'pipeDelimited': // pipe separated values foo|bar. + items = val.split('|'); + break; + case 'simple': // comma separated values foo,bar. + case 'form': // comma separated values foo,bar. + items = val.split(','); + break; + case 'deepObject': + 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 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..660b1726df9b 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,16 +108,19 @@ 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': // TODO(jannyhou) to support `cookie`, // see issue https://github.com/strongloop/loopback-next/issues/997 default: diff --git a/packages/rest/test/unit/deserializer.test.ts b/packages/rest/test/unit/deserializer.test.ts new file mode 100644 index 000000000000..d05002ead117 --- /dev/null +++ b/packages/rest/test/unit/deserializer.test.ts @@ -0,0 +1,234 @@ +// 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-v3-types'; + +// tslint:disable:no-any +describe('deserializer', () => { + it('converts number parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'balance', + schema: { + 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', + schema: { + 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', + schema: { + 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', + schema: { + 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', + schema: { + 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', + schema: { + 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', + schema: { + type: 'string', + }, + }; + expectToDeserialize(param, ['', 'A'], ['', 'A']); + expectToDeserializeNullOrUndefined(param); + }); + + it('reports errors for invalid string parameters', () => { + const param: ParameterObject = { + in: 'query', + name: 'name', + schema: { + 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', + schema: { + 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', + style: 'simple', + schema: { + type: 'array', + 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', + style: 'spaceDelimited', + schema: { + type: 'array', + 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', + style: 'pipeDelimited', + schema: { + type: 'array', + items: { + type: 'string', + }, + }, + }; + expectToDeserialize( + param, + ['1|2|3', 'ab|c'], + [['1', '2', '3'], ['ab', 'c']], + ); + }); + }); + + describe('number[]', () => { + it('converts csv format', () => { + const param: ParameterObject = { + in: 'query', + name: 'nums', + style: 'simple', + schema: { + type: 'array', + 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); + } + } +});