Skip to content

Commit

Permalink
feat(rest): add basic parameter type conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Feb 5, 2018
1 parent de969e4 commit f587cf5
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 6 deletions.
102 changes: 102 additions & 0 deletions packages/rest/src/converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// 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 converter for parameters
*/
// tslint:disable-next-line:no-any
export function convert(val: any, param: ParameterObject): any {
if (param.in === 'body') {
param = getBodyDescriptor(param);
}
const type = param.type;
if (val == null) {
if (param.required) {
throw new Error(
`Value is not provided for required parameter ${param.name}`,
);
}
return undefined;
}
switch (type) {
case 'string':
if (param.format === 'date' || param.format === 'date-time') {
return new Date(val);
}
return String(val);
case 'number':
case 'integer':
const num = Number(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 (val === 'false') return false;
if (val === 'true') return true;
if (typeof val === 'boolean') return val;
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 => convert(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 './converter';

export {writeResultToResponse} from './writer';

Expand Down
18 changes: 12 additions & 6 deletions packages/rest/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@

import {ServerRequest} from 'http';
import * as HttpErrors from 'http-errors';
import {OperationObject, ParameterObject} from '@loopback/openapi-spec';
import {
OperationObject,
ParameterObject,
ParameterType,
} from '@loopback/openapi-spec';
import {promisify} from 'util';
import {
OperationArgs,
ParsedRequest,
PathParameterValues,
} from './internal-types';
import {ResolvedRoute} from './router/routing-table';
import {convert} from './converter';

type HttpError = HttpErrors.HttpError;

// tslint:disable-next-line:no-any
Expand Down Expand Up @@ -104,19 +110,19 @@ function buildOperationArguments(
const spec = paramSpec as ParameterObject;
switch (spec.in) {
case 'query':
args.push(request.query[spec.name]);
args.push(convert(request.query[spec.name], spec));
break;
case 'path':
args.push(pathParams[spec.name]);
args.push(convert(pathParams[spec.name], spec));
break;
case 'header':
args.push(request.headers[spec.name.toLowerCase()]);
args.push(convert(request.headers[spec.name.toLowerCase()], spec));
break;
case 'formData':
args.push(body ? body[spec.name] : undefined);
args.push(body ? convert(body[spec.name], spec) : undefined);
break;
case 'body':
args.push(body);
args.push(convert(body, spec));
break;
default:
throw new HttpErrors.NotImplemented(
Expand Down
176 changes: 176 additions & 0 deletions packages/rest/test/unit/converter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// 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 {convert} from '../..';
import {expect, ShotRequest, ShotRequestOptions} from '@loopback/testlab';
import {OperationObject, ParameterObject} from '@loopback/openapi-spec';

// tslint:disable:no-any
describe('converter', () => {
function expectToConvert(
param: ParameterObject,
source: any[],
target: any[],
) {
expect(source.map(i => convert(i, param))).to.eql(target);
}

function expectToFail(
param: ParameterObject,
source: any[],
reason: string | RegExp,
) {
for (const i of source) {
expect(() => convert(i, param)).to.throw(reason);
}
}

it('converts number parameters', () => {
const param: ParameterObject = {
in: 'query',
name: 'balance',
type: 'number',
};
expectToConvert(
param,
[0, 1.5, '10', '2.5', true, false],
[0, 1.5, 10, 2.5, 1, 0],
);
expectToFail(
param,
['a', 'a1', 'true', {}],
/Invalid value .* for parameter balance\: number/,
);
});

it('converts integer parameters', () => {
const param: ParameterObject = {
in: 'query',
name: 'id',
type: 'integer',
};
expectToConvert(param, [0, -1, '10', '-5'], [0, -1, 10, -5]);
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',
};
expectToConvert(
param,
[true, false, 'true', 'false'],
[true, false, true, false],
);
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',
};
expectToConvert(
param,
[true, false, 0, -1, 2.5, '', 'A'],
['true', 'false', '0', '-1', '2.5', '', 'A'],
);
});

it('converts date parameters', () => {
const param: ParameterObject = {
in: 'query',
name: 'date',
type: 'string',
format: 'date',
};
const date = new Date();
expectToConvert(param, [date.toJSON()], [date]);
});

describe('string[]', () => {
it('converts csv format', () => {
const param: ParameterObject = {
in: 'query',
name: 'nums',
type: 'array',
collectionFormat: 'csv',
items: {
type: 'string',
},
};
expectToConvert(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',
},
};
expectToConvert(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',
},
};
expectToConvert(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',
},
};
expectToConvert(
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',
},
};
expectToConvert(param, ['1,2,3', '-10,2.5'], [[1, 2, 3], [-10, 2.5]]);
});
});
});

0 comments on commit f587cf5

Please sign in to comment.