Skip to content

Commit

Permalink
feat(rest): add basic HTTP parameter deserialization
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Mar 14, 2018
1 parent c3cd5e6 commit 4e2dc0f
Show file tree
Hide file tree
Showing 4 changed files with 350 additions and 3 deletions.
114 changes: 114 additions & 0 deletions packages/rest/src/deserializer.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
1 change: 1 addition & 0 deletions packages/rest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export * from './providers';
import * as HttpErrors from 'http-errors';

export * from './parser';
export * from './deserializer';

export {writeResultToResponse} from './writer';

Expand Down
13 changes: 10 additions & 3 deletions packages/rest/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
225 changes: 225 additions & 0 deletions packages/rest/test/unit/deserializer.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
});

0 comments on commit 4e2dc0f

Please sign in to comment.