Skip to content

Commit

Permalink
Allow different json media types for params, requests, and responses
Browse files Browse the repository at this point in the history
  • Loading branch information
ckeboss committed Nov 27, 2019
1 parent 9c4c57f commit 55cb8ac
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 38 deletions.
13 changes: 9 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 0 additions & 14 deletions src/middlewares/ajv/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {},
Expand Down Expand Up @@ -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;
}
23 changes: 17 additions & 6 deletions src/middlewares/openapi.request.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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) {
Expand Down
33 changes: 23 additions & 10 deletions src/middlewares/openapi.response.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <any[]>Object.entries(responses)) {
const [name, response] = entry;
if (!canValidate(response)) {
for (const [name, response] of <any[]>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#",
Expand All @@ -126,6 +138,7 @@ export class ResponseValidator {
};
}


const validators = {};
for (const [name, schema] of Object.entries(schemas)) {
validators[name] = this.ajv.compile(<object>schema);
Expand Down
1 change: 1 addition & 0 deletions test/common/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export async function createApp(
(<any>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());
Expand Down
21 changes: 19 additions & 2 deletions test/request.bodies.ref.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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 => {
Expand Down Expand Up @@ -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`)
Expand Down
10 changes: 8 additions & 2 deletions test/resources/request.bodies.ref.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,7 +39,7 @@ components:
example: +15017122661
format: phone-number
required:
- testProperty
- testProperty

requestBodies:
TestBody:
Expand All @@ -48,3 +51,6 @@ components:
application/json:
schema:
$ref: '#/components/schemas/Test'
application/hal+json:
schema:
$ref: '#/components/schemas/Test'

0 comments on commit 55cb8ac

Please sign in to comment.