Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat(rest): add basic parameter type conversion #941

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, 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,
);
}
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
11 changes: 8 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,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:
Expand Down
234 changes: 234 additions & 0 deletions packages/rest/test/unit/deserializer.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
});