diff --git a/package-lock.json b/package-lock.json index 077d7e36..d638702d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2259,10 +2259,10 @@ "p-defer": "^1.0.0" } }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "media-type": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/media-type/-/media-type-0.3.1.tgz", + "integrity": "sha1-XVac3QxS2cQcfGRRlz7dJn+yG8s=" }, "mem": { "version": "4.3.0", @@ -3650,6 +3650,11 @@ "mime-types": "~2.1.24" }, "dependencies": { + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, "mime-db": { "version": "1.40.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", diff --git a/package.json b/package.json index 511e8626..36f71303 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "js-yaml": "^3.13.1", "lodash": "^4.17.15", "lodash.merge": "^4.6.2", + "media-type": "^0.3.1", "multer": "^1.4.2", "ono": "^5.0.1", "path-to-regexp": "^6.0.0", diff --git a/src/middlewares/ajv/index.ts b/src/middlewares/ajv/index.ts index 3c551d09..3f6a40bd 100644 --- a/src/middlewares/ajv/index.ts +++ b/src/middlewares/ajv/index.ts @@ -4,8 +4,6 @@ import { formats } from './formats'; import { OpenAPIV3 } from '../../framework/types'; import ajv = require('ajv'); -const TYPE_JSON = 'application/json'; - export function createRequestAjv( openApiSpec: OpenAPIV3.Document, options: ajv.Options = {}, @@ -95,17 +93,5 @@ function createAjv( ); } - if (openApiSpec.components.requestBodies) { - Object.entries(openApiSpec.components.requestBodies).forEach( - ([id, schema]: any[]) => { - // TODO add support for content all content types - ajv.addSchema( - schema.content[TYPE_JSON].schema, - `#/components/requestBodies/${id}`, - ); - }, - ); - } - return ajv; } diff --git a/src/middlewares/openapi.request.validator.ts b/src/middlewares/openapi.request.validator.ts index aed0db00..ca5881ee 100644 --- a/src/middlewares/openapi.request.validator.ts +++ b/src/middlewares/openapi.request.validator.ts @@ -15,8 +15,7 @@ import { ValidateRequestOpts, } from '../framework/types'; import { Ajv } from 'ajv'; - -const TYPE_JSON = 'application/json'; +import * as mediaTypeParser from 'media-type'; export class RequestValidator { private _middlewareCache; @@ -246,7 +245,7 @@ export class RequestValidator { : []; } - private parametersToSchema(path, parameters = []) { + private parametersToSchema(path: string, parameters = []) { const schema = { query: {}, headers: {}, params: {}, cookies: {} }; const reqFields = { query: 'query', @@ -280,9 +279,21 @@ export class RequestValidator { } let parameterSchema = parameter.schema; - if (parameter.content && parameter.content[TYPE_JSON]) { - parameterSchema = parameter.content[TYPE_JSON].schema; - parseJson.push({ name, reqField }); + if (parameter.content) { + /** + * Per the OpenAPI3 spec: + * A map containing the representations for the parameter. The key is the media type + * and the value describes it. The map MUST only contain one entry. + * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterContent + */ + const mediaType = Object.keys(parameter.content)[0] + const mediaTypeParsed = mediaTypeParser.fromString(mediaType) + + parameterSchema = parameter.content[mediaType].schema; + + if (mediaTypeParsed.subtype === 'json' || mediaTypeParsed.suffix === 'json') { + parseJson.push({ name, reqField }); + } } if (!parameterSchema) { diff --git a/src/middlewares/openapi.response.validator.ts b/src/middlewares/openapi.response.validator.ts index 261987eb..9e76ff7f 100644 --- a/src/middlewares/openapi.response.validator.ts +++ b/src/middlewares/openapi.response.validator.ts @@ -9,8 +9,7 @@ import { validationError, } from './util'; import { OpenAPIV3 } from '../framework/types'; - -const TYPE_JSON = 'application/json'; +import * as mediaTypeParser from 'media-type'; export class ResponseValidator { private ajv: Ajv.Ajv; @@ -100,21 +99,34 @@ export class ResponseValidator { * @returns a map of validators */ private buildValidators(responses) { - const canValidate = r => - typeof r.content === 'object' && - r.content[TYPE_JSON] && - r.content[TYPE_JSON].schema; + const canValidate = response => { + if (typeof response.content !== 'object') { + return false; + } + for (let mediaType of Object.keys(response.content)) { + const mediaTypeParsed = mediaTypeParser.fromString(mediaType); + + if (mediaTypeParsed.subtype === 'json' || mediaTypeParsed.suffix === 'json') { + return response.content[mediaType] && + response.content[mediaType].schema ? mediaType : false; + } + } + + return false; + } const schemas = {}; - for (const entry of Object.entries(responses)) { - const [name, response] = entry; - if (!canValidate(response)) { + for (const [name, response] of Object.entries(responses)) { + const mediaTypeToValidate = canValidate(response); + + if (!mediaTypeToValidate) { // TODO support content other than JSON // don't validate // assume is valid continue; } - const schema = response.content[TYPE_JSON].schema; + const schema = response.content[mediaTypeToValidate].schema; + schemas[name] = { // $schema: 'http://json-schema.org/schema#', // $schema: "http://json-schema.org/draft-04/schema#", @@ -126,6 +138,7 @@ export class ResponseValidator { }; } + const validators = {}; for (const [name, schema] of Object.entries(schemas)) { validators[name] = this.ajv.compile(schema); diff --git a/test/common/app.ts b/test/common/app.ts index 582f5fba..9a90cbd5 100644 --- a/test/common/app.ts +++ b/test/common/app.ts @@ -18,6 +18,7 @@ export async function createApp( (app).basePath = '/v1'; app.use(bodyParser.json()); + app.use(bodyParser.json({type: 'application/hal+json'})); app.use(bodyParser.text()); app.use(logger('dev')); app.use(express.json()); diff --git a/test/request.bodies.ref.spec.ts b/test/request.bodies.ref.spec.ts index a9c3b7ee..248e8692 100644 --- a/test/request.bodies.ref.spec.ts +++ b/test/request.bodies.ref.spec.ts @@ -1,5 +1,4 @@ import * as path from 'path'; -import * as express from 'express'; import { expect } from 'chai'; import * as request from 'supertest'; import { createApp } from './common/app'; @@ -22,8 +21,10 @@ describe(packageJson.name, () => { app => { // Define new coercion routes app.post(`${app.basePath}/request_bodies_ref`, (req, res) => { - if (req.headers['content-type'].indexOf('text/plain') > -1) { + if (req.header('accept') && req.header('accept').indexOf('text/plain') > -1) { res.type('text').send(req.body); + } else if (req.header('accept') && req.header('accept').indexOf('application/hal+json') > -1) { + res.type('application/hal+json').send(req.body); } else if (req.query.bad_body) { const r = req.body; r.unexpected_prop = 'bad'; @@ -46,6 +47,7 @@ describe(packageJson.name, () => { return request(app) .post(`${app.basePath}/request_bodies_ref`) .set('content-type', 'text/plain') + .set('accept', 'text/plain') .send(stringData) .expect(200) .then(r => { @@ -79,6 +81,21 @@ describe(packageJson.name, () => { expect(body).to.have.property('testProperty'); })); + it('should return 200 if a json suffex is used for content-type', async () => + request(app) + .post(`${app.basePath}/request_bodies_ref`) + .set('accept', 'application/hal+json') + .set('content-type', 'application/hal+json') + .send({ + testProperty: 'abc', + }) + .expect(200) + .then(r => { + const { body } = r; + expect(r.get('content-type')).to.contain('application/hal+json') + expect(body).to.have.property('testProperty'); + })); + it('should return 500 if additional response body property is returned', async () => request(app) .post(`${app.basePath}/request_bodies_ref`) diff --git a/test/resources/request.bodies.ref.yaml b/test/resources/request.bodies.ref.yaml index 8d86cecf..41f53357 100644 --- a/test/resources/request.bodies.ref.yaml +++ b/test/resources/request.bodies.ref.yaml @@ -20,7 +20,10 @@ paths: schema: type: string application/json: - schema: + schema: + $ref: '#/components/schemas/Test' + application/hal+json: + schema: $ref: '#/components/schemas/Test' '400': description: Bad Request @@ -36,7 +39,7 @@ components: example: +15017122661 format: phone-number required: - - testProperty + - testProperty requestBodies: TestBody: @@ -48,3 +51,6 @@ components: application/json: schema: $ref: '#/components/schemas/Test' + application/hal+json: + schema: + $ref: '#/components/schemas/Test'