Skip to content

open api 3 - discriminator #41

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

Merged
merged 9 commits into from
Jan 31, 2019
Merged
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
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