Skip to content

Commit

Permalink
open api 3 - discriminator (#41)
Browse files Browse the repository at this point in the history
Add support in OpenApi 3.0
  • Loading branch information
manorlh authored and idanto committed Jan 31, 2019
1 parent 1cae64c commit f706e26
Show file tree
Hide file tree
Showing 23 changed files with 1,622 additions and 227 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ module.exports = inputValidation.init('test/pet-store-swagger.yaml', {framework:
- koa support - When using this package as middleware for koa, the validations errors are being thrown.
- koa packages - This package supports koa server that uses [`koa-router`](https://www.npmjs.com/package/koa-router), [`koa-bodyparser`](https://www.npmjs.com/package/koa-bodyparser) and [`koa-multer`](https://www.npmjs.com/package/koa-multer)

## Open api 3 - known issues
- supporting inheritance with discriminator , only if the ancestor object is the discriminator.
- The discriminator supports in the inheritance chain stop when getting to a child with no discriminator (a leaf in the inheritance tree), meaning a leaf can't have a field which starts a new inheritance tree.
so child with no discriminator cant point to other child with discriminator,

## Running Tests
Using mocha, istanbul and mochawesome
```bash
Expand Down
285 changes: 226 additions & 59 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@
"author": "Idan Tovi",
"license": "MIT",
"dependencies": {
"ajv": "^5.5.2",
"swagger-parser": "^4.0.1"
"ajv": "^6.6.2",
"clone-deep": "^4.0.1",
"swagger-parser": "^6.0.2"
},
"devDependencies": {
"body-parser": "^1.18.2",
Expand Down
40 changes: 40 additions & 0 deletions src/data_structures/tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

'use strict';
/** Class representing a node in a tree structure, which each node has value and children(nodes) saving by key. */
class Node {
/**
* Create a node.
* @param value - The value of the node.
*/
constructor(value){
this.value = value;
this.childrenAsKeyValue = {};
}
/**
* Add child to the node.
* @param node - The node which going to be the child.
* @param key - The key which is the identifier of the child.
*/
addChild(node, key){
this.childrenAsKeyValue[key] = node;
}
/**
* Override node data by other node by reference.
* @param node - The node which going to use to take his data.
*/
setData(node){
if (node instanceof Node){
this.value = node.value;
this.childrenAsKeyValue = node.childrenAsKeyValue;
}
};
/**
* Get node value.
* @return The value of the node.
*/
getValue(){
return this.value;
}
}

module.exports = {Node};
122 changes: 22 additions & 100 deletions src/middleware.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
'use strict';

var SwaggerParser = require('swagger-parser'),
Ajv = require('ajv'),
Validators = require('./utils/validators'),
filesKeyword = require('./customKeywords/files'),
contentKeyword = require('./customKeywords/contentTypeValidation'),
InputValidationError = require('./inputValidationError'),
schemaPreprocessor = require('./utils/schema-preprocessor'),
swagger3 = require('./swagger3/open-api3'),
swagger2 = require('./swagger2'),
ajvUtils = require('./utils/ajv-utils'),
Ajv = require('ajv'),
sourceResolver = require('./utils/sourceResolver');

var schemas = {};
var middlewareOptions;
var ajvConfigBody;
var ajvConfigParams;
var framework;

/**
Expand All @@ -22,8 +19,6 @@ var framework;
*/
function init(swaggerPath, options) {
middlewareOptions = options || {};
ajvConfigBody = middlewareOptions.ajvConfigBody || {};
ajvConfigParams = middlewareOptions.ajvConfigParams || {};
framework = middlewareOptions.framework ? require(`./frameworks/${middlewareOptions.framework}`) : require('./frameworks/express');
const makeOptionalAttributesNullable = middlewareOptions.makeOptionalAttributesNullable || false;

Expand All @@ -39,19 +34,21 @@ function init(swaggerPath, options) {
Object.keys(dereferenced.paths[currentPath]).filter(function (parameter) { return parameter !== 'parameters' })
.forEach(function (currentMethod) {
schemas[parsedPath][currentMethod.toLowerCase()] = {};

const isOpenApi3 = dereferenced.openapi === '3.0.0';
const parameters = dereferenced.paths[currentPath][currentMethod].parameters || [];

let bodySchema = middlewareOptions.expectFormFieldsInBody
? parameters.filter(function (parameter) { return (parameter.in === 'body' || (parameter.in === 'formData' && parameter.type !== 'file')) })
: parameters.filter(function (parameter) { return parameter.in === 'body' });

if (makeOptionalAttributesNullable) {
schemaPreprocessor.makeOptionalAttributesNullable(bodySchema);
}
if (bodySchema.length > 0) {
const validatedBodySchema = _getValidatedBodySchema(bodySchema);
schemas[parsedPath][currentMethod].body = buildBodyValidation(validatedBodySchema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath);
if (isOpenApi3){
schemas[parsedPath][currentMethod].body = swagger3.buildBodyValidation(dereferenced, swaggers[1], currentPath, currentMethod, middlewareOptions);
} else {
let bodySchema = middlewareOptions.expectFormFieldsInBody
? parameters.filter(function (parameter) { return (parameter.in === 'body' || (parameter.in === 'formData' && parameter.type !== 'file')) })
: parameters.filter(function (parameter) { return parameter.in === 'body' });
if (makeOptionalAttributesNullable) {
schemaPreprocessor.makeOptionalAttributesNullable(bodySchema);
}
if (bodySchema.length > 0) {
const validatedBodySchema = swagger2.getValidatedBodySchema(bodySchema);
schemas[parsedPath][currentMethod].body = swagger2.buildBodyValidation(validatedBodySchema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath, middlewareOptions);
}
}

let localParameters = parameters.filter(function (parameter) {
Expand All @@ -60,7 +57,7 @@ function init(swaggerPath, options) {

if (localParameters.length > 0 || middlewareOptions.contentTypeValidation) {
schemas[parsedPath][currentMethod].parameters = buildParametersValidation(localParameters,
dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes);
dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes, middlewareOptions);
}
});
});
Expand All @@ -69,32 +66,6 @@ function init(swaggerPath, options) {
return Promise.reject(error);
});
}

function _getValidatedBodySchema(bodySchema) {
let validatedBodySchema;
if (bodySchema[0].in === 'body') {
// if we are processing schema for a simple JSON payload, no additional processing needed
validatedBodySchema = bodySchema[0].schema;
} else if (bodySchema[0].in === 'formData') {
// if we are processing multipart form, assemble body schema from form field schemas
validatedBodySchema = {
required: [],
properties: {}
};
bodySchema.forEach((formField) => {
if (formField.type !== 'file') {
validatedBodySchema.properties[formField.name] = {
type: formField.type
};
if (formField.required) {
validatedBodySchema.required.push(formField.name);
}
}
});
}
return validatedBodySchema;
}

/**
* The middleware - should be called for each express route
* @param {any} req
Expand Down Expand Up @@ -141,55 +112,6 @@ function _validateParams(headers, pathParams, query, files, path, method) {
});
}

function addCustomKeyword(ajv, formats) {
if (formats) {
formats.forEach(function (format) {
ajv.addFormat(format.name, format.pattern);
});
}

ajv.addKeyword('files', filesKeyword);
ajv.addKeyword('content', contentKeyword);
}

function buildBodyValidation(schema, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath) {
const defaultAjvOptions = {
allErrors: true
// unknownFormats: 'ignore'
};
const options = Object.assign({}, defaultAjvOptions, ajvConfigBody);
let ajv = new Ajv(options);

addCustomKeyword(ajv, middlewareOptions.formats);

if (schema.discriminator) {
return buildInheritance(schema.discriminator, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, ajv);
} else {
return new Validators.SimpleValidator(ajv.compile(schema));
}
}

function buildInheritance(discriminator, dereferencedDefinitions, swagger, currentPath, currentMethod, parsedPath, ajv) {
let bodySchema = swagger.paths[currentPath][currentMethod].parameters.filter(function (parameter) { return parameter.in === 'body' })[0];
var inheritsObject = {
inheritance: []
};
inheritsObject.discriminator = discriminator;

Object.keys(swagger.definitions).forEach(key => {
if (swagger.definitions[key].allOf) {
swagger.definitions[key].allOf.forEach(element => {
if (element['$ref'] && element['$ref'] === bodySchema.schema['$ref']) {
inheritsObject[key] = ajv.compile(dereferencedDefinitions[key]);
inheritsObject.inheritance.push(key);
}
});
}
}, this);

return new Validators.OneOfValidator(inheritsObject);
}

function createContentTypeHeaders(validate, contentTypes) {
if (!validate || !contentTypes) return;

Expand All @@ -198,16 +120,16 @@ function createContentTypeHeaders(validate, contentTypes) {
};
}

function buildParametersValidation(parameters, contentTypes) {
function buildParametersValidation(parameters, contentTypes, middlewareOptions) {
const defaultAjvOptions = {
allErrors: true,
coerceTypes: 'array'
// unknownFormats: 'ignore'
};
const options = Object.assign({}, defaultAjvOptions, ajvConfigParams);
const options = Object.assign({}, defaultAjvOptions, middlewareOptions.ajvConfigParams);
let ajv = new Ajv(options);

addCustomKeyword(ajv, middlewareOptions.formats);
ajvUtils.addCustomKeyword(ajv, middlewareOptions.formats);

var ajvParametersSchema = {
title: 'HTTP parameters',
Expand Down
72 changes: 72 additions & 0 deletions src/swagger2/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@

const Validators = require('../validators'),
Ajv = require('ajv'),
ajvUtils = require('../utils/ajv-utils');

module.exports = {
getValidatedBodySchema,
buildBodyValidation
};

function getValidatedBodySchema(bodySchema) {
let validatedBodySchema;
if (bodySchema[0].in === 'body') {
// if we are processing schema for a simple JSON payload, no additional processing needed
validatedBodySchema = bodySchema[0].schema;
} else if (bodySchema[0].in === 'formData') {
// if we are processing multipart form, assemble body schema from form field schemas
validatedBodySchema = {
required: [],
properties: {}
};
bodySchema.forEach((formField) => {
if (formField.type !== 'file') {
validatedBodySchema.properties[formField.name] = {
type: formField.type
};
if (formField.required) {
validatedBodySchema.required.push(formField.name);
}
}
});
}
return validatedBodySchema;
}

function buildBodyValidation(schema, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, middlewareOptions = {}) {
const defaultAjvOptions = {
allErrors: true
// unknownFormats: 'ignore'
};
const options = Object.assign({}, defaultAjvOptions, middlewareOptions.ajvConfigBody);
let ajv = new Ajv(options);

ajvUtils.addCustomKeyword(ajv, middlewareOptions.formats);

if (schema.discriminator) {
return buildInheritance(schema.discriminator, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, ajv);
} else {
return new Validators.SimpleValidator(ajv.compile(schema));
}
}

function buildInheritance(discriminator, dereferencedDefinitions, swagger, currentPath, currentMethod, parsedPath, ajv) {
let bodySchema = swagger.paths[currentPath][currentMethod].parameters.filter(function (parameter) { return parameter.in === 'body' })[0];
var inheritsObject = {
inheritance: []
};
inheritsObject.discriminator = discriminator;

Object.keys(swagger.definitions).forEach(key => {
if (swagger.definitions[key].allOf) {
swagger.definitions[key].allOf.forEach(element => {
if (element['$ref'] && element['$ref'] === bodySchema.schema['$ref']) {
inheritsObject[key] = ajv.compile(dereferencedDefinitions[key]);
inheritsObject.inheritance.push(key);
}
});
}
}, this);

return new Validators.OneOfValidator(inheritsObject);
}
Loading

0 comments on commit f706e26

Please sign in to comment.