-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
588 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// Copyright IBM Corp. 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 {ParameterObject, isReferenceObject} from '@loopback/openapi-v3-types'; | ||
import {Validator} from './validator'; | ||
import * as debugModule from 'debug'; | ||
import {RestHttpErrors} from '../'; | ||
|
||
const debug = debugModule('loopback:rest:coercion'); | ||
|
||
/** | ||
* Coerce the http raw data to a JavaScript type data of a parameter | ||
* according to its OpenAPI schema specification. | ||
* | ||
* @param data The raw data get from http request | ||
* @param schema The parameter's schema defined in OpenAPI specification | ||
*/ | ||
export function coerceParameter(data: string, spec: ParameterObject) { | ||
const schema = spec.schema; | ||
if (!schema || isReferenceObject(schema)) { | ||
debug( | ||
'The parameter with schema %s is not coerced since schema' + | ||
'dereference is not supported yet.', | ||
schema, | ||
); | ||
return data; | ||
} | ||
const OAIType = getOAIPrimitiveType(schema.type, schema.format); | ||
const validator = new Validator({parameterSpec: spec}); | ||
|
||
validator.validateParamBeforeCoercion(data); | ||
|
||
switch (OAIType) { | ||
case 'byte': | ||
return Buffer.from(data, 'base64'); | ||
case 'date': | ||
return new Date(data); | ||
case 'float': | ||
case 'double': | ||
return parseFloat(data); | ||
case 'number': | ||
const coercedData = data ? Number(data) : undefined; | ||
if (coercedData === undefined) return; | ||
if (isNaN(coercedData)) throw RestHttpErrors.invalidData(data, spec.name); | ||
return coercedData; | ||
case 'long': | ||
return Number(data); | ||
case 'integer': | ||
return parseInt(data); | ||
case 'boolean': | ||
return isTrue(data) ? true : isFalse(data) ? false : undefined; | ||
case 'string': | ||
case 'password': | ||
// serialize will be supported in next PR | ||
case 'serialize': | ||
default: | ||
return data; | ||
} | ||
} | ||
|
||
/** | ||
* A set of truthy values. A data in this set will be coerced to `true`. | ||
* | ||
* @param data The raw data get from http request | ||
* @returns The corresponding coerced boolean type | ||
*/ | ||
function isTrue(data: string): boolean { | ||
return ['true', '1'].includes(data); | ||
} | ||
|
||
/** | ||
* A set of falsy values. A data in this set will be coerced to `false`. | ||
* @param data The raw data get from http request | ||
* @returns The corresponding coerced boolean type | ||
*/ | ||
function isFalse(data: string): boolean { | ||
return ['false', '0'].includes(data); | ||
} | ||
|
||
/** | ||
* Return the corresponding OpenAPI data type given an OpenAPI schema | ||
* | ||
* @param type The type in an OpenAPI schema specification | ||
* @param format The format in an OpenAPI schema specification | ||
*/ | ||
function getOAIPrimitiveType(type?: string, format?: string) { | ||
// serizlize will be supported in next PR | ||
if (type === 'object' || type === 'array') return 'serialize'; | ||
if (type === 'string') { | ||
switch (format) { | ||
case 'byte': | ||
return 'byte'; | ||
case 'binary': | ||
return 'binary'; | ||
case 'date': | ||
return 'date'; | ||
case 'date-time': | ||
return 'date-time'; | ||
case 'password': | ||
return 'password'; | ||
default: | ||
return 'string'; | ||
} | ||
} | ||
if (type === 'boolean') return 'boolean'; | ||
if (type === 'number') | ||
return format === 'float' | ||
? 'float' | ||
: format === 'double' | ||
? 'double' | ||
: 'number'; | ||
if (type === 'integer') return format === 'int64' ? 'long' : 'integer'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import * as HttpErrors from 'http-errors'; | ||
export namespace RestHttpErrors { | ||
export function invalidData<T>(data: T, name: string) { | ||
const msg = `Invalid data ${JSON.stringify(data)} for parameter ${name}!`; | ||
return new HttpErrors.BadRequest(msg); | ||
} | ||
export function missingRequired(name: string): HttpErrors.HttpError { | ||
const msg = `Required parameter ${name} is missing!`; | ||
return new HttpErrors.BadRequest(msg); | ||
} | ||
export function invalidParamLocation(location: string): HttpErrors.HttpError { | ||
return new HttpErrors.NotImplemented( | ||
'Parameters with "in: ' + location + '" are not supported yet.', | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// Copyright IBM Corp. 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 {ParameterObject} from '@loopback/openapi-v3-types'; | ||
import {RestHttpErrors} from '../'; | ||
|
||
/** | ||
* A set of options to pass into the validator functions | ||
*/ | ||
export type ValidationOptions = { | ||
required?: boolean; | ||
}; | ||
|
||
/** | ||
* The context information that a validator needs | ||
*/ | ||
export type ValidationContext = { | ||
parameterSpec: ParameterObject; | ||
}; | ||
|
||
/** | ||
* Validator class provides a bunch of functions that perform | ||
* validations on the request parameters and request body. | ||
*/ | ||
export class Validator { | ||
constructor(public ctx: ValidationContext) {} | ||
|
||
/** | ||
* The validation executed before type coercion. Like | ||
* checking absence. | ||
* | ||
* @param type A parameter's type. | ||
* @param value A parameter's raw value from http request. | ||
* @param opts options | ||
*/ | ||
validateParamBeforeCoercion( | ||
value: string | object | undefined, | ||
opts?: ValidationOptions, | ||
) { | ||
if (this.isAbsent(value) && this.isRequired(opts)) { | ||
const name = this.ctx.parameterSpec.name; | ||
throw RestHttpErrors.missingRequired(name); | ||
} | ||
} | ||
|
||
/** | ||
* Check is a parameter required or not. | ||
* | ||
* @param opts | ||
*/ | ||
isRequired(opts?: ValidationOptions) { | ||
if (this.ctx.parameterSpec.required) return true; | ||
if (opts && opts.required) return true; | ||
return false; | ||
} | ||
|
||
/** | ||
* Return `true` if the value is empty, return `false` otherwise. | ||
* | ||
* @param value | ||
*/ | ||
// tslint:disable-next-line:no-any | ||
isAbsent(value: any) { | ||
return value === '' || value === undefined; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
packages/rest/test/acceptance/coercion/coercion.acceptance.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import {supertest, createClientForHandler, sinon} from '@loopback/testlab'; | ||
import {RestApplication, get, param} from '../../..'; | ||
|
||
describe('Coercion', () => { | ||
let app: RestApplication; | ||
let client: supertest.SuperTest<supertest.Test>; | ||
|
||
before(givenAClient); | ||
|
||
after(async () => { | ||
await app.stop(); | ||
}); | ||
|
||
class MyController { | ||
@get('/create-number-from-path/{num}') | ||
createNumberFromPath(@param.path.number('num') num: number) { | ||
return num; | ||
} | ||
|
||
@get('/create-number-from-query') | ||
createNumberFromQuery(@param.query.number('num') num: number) { | ||
return num; | ||
} | ||
|
||
@get('/create-number-from-header') | ||
createNumberFromHeader(@param.header.number('num') num: number) { | ||
return num; | ||
} | ||
} | ||
|
||
it('coerces parameter in path from string to number', async () => { | ||
const spy = sinon.spy(MyController.prototype, 'createNumberFromPath'); | ||
await client.get('/create-number-from-path/100').expect(200); | ||
sinon.assert.calledWithExactly(spy, 100); | ||
}); | ||
|
||
it('coerces parameter in header from string to number', async () => { | ||
const spy = sinon.spy(MyController.prototype, 'createNumberFromHeader'); | ||
await client.get('/create-number-from-header').set({num: 100}); | ||
sinon.assert.calledWithExactly(spy, 100); | ||
}); | ||
|
||
it('coerces parameter in query from string to number', async () => { | ||
const spy = sinon.spy(MyController.prototype, 'createNumberFromQuery'); | ||
await client | ||
.get('/create-number-from-query') | ||
.query({num: 100}) | ||
.expect(200); | ||
sinon.assert.calledWithExactly(spy, 100); | ||
}); | ||
|
||
async function givenAClient() { | ||
app = new RestApplication(); | ||
app.controller(MyController); | ||
await app.start(); | ||
client = await createClientForHandler(app.requestHandler); | ||
} | ||
}); |
18 changes: 18 additions & 0 deletions
18
packages/rest/test/unit/coercion/invalid-spec.unit.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Copyright IBM Corp. 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 {test} from './utils'; | ||
import {RestHttpErrors} from '../../../'; | ||
import {ParameterLocation} from '@loopback/openapi-v3-types'; | ||
|
||
const INVALID_PARAM = { | ||
in: <ParameterLocation>'unknown', | ||
name: 'aparameter', | ||
schema: {type: 'unknown'}, | ||
}; | ||
|
||
describe('throws error for invalid parameter spec', () => { | ||
test(INVALID_PARAM, '', RestHttpErrors.invalidParamLocation('unknown')); | ||
}); |
19 changes: 19 additions & 0 deletions
19
packages/rest/test/unit/coercion/paramStringToBoolean.unit.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Copyright IBM Corp. 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 {test} from './utils'; | ||
import {ParameterLocation} from '@loopback/openapi-v3-types'; | ||
|
||
const BOOLEAN_PARAM = { | ||
in: <ParameterLocation>'path', | ||
name: 'aparameter', | ||
schema: {type: 'boolean'}, | ||
}; | ||
|
||
describe('coerce param from string to boolean', () => { | ||
test(BOOLEAN_PARAM, 'false', false); | ||
test(BOOLEAN_PARAM, 'true', true); | ||
test(BOOLEAN_PARAM, undefined, undefined); | ||
}); |
Oops, something went wrong.