Skip to content

Commit f706e26

Browse files
manorlhidanto
authored andcommitted
open api 3 - discriminator (#41)
Add support in OpenApi 3.0
1 parent 1cae64c commit f706e26

23 files changed

+1622
-227
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ module.exports = inputValidation.init('test/pet-store-swagger.yaml', {framework:
156156
- koa support - When using this package as middleware for koa, the validations errors are being thrown.
157157
- 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)
158158

159+
## Open api 3 - known issues
160+
- supporting inheritance with discriminator , only if the ancestor object is the discriminator.
161+
- 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.
162+
so child with no discriminator cant point to other child with discriminator,
163+
159164
## Running Tests
160165
Using mocha, istanbul and mochawesome
161166
```bash

package-lock.json

Lines changed: 226 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@
5050
"author": "Idan Tovi",
5151
"license": "MIT",
5252
"dependencies": {
53-
"ajv": "^5.5.2",
54-
"swagger-parser": "^4.0.1"
53+
"ajv": "^6.6.2",
54+
"clone-deep": "^4.0.1",
55+
"swagger-parser": "^6.0.2"
5556
},
5657
"devDependencies": {
5758
"body-parser": "^1.18.2",

src/data_structures/tree.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
'use strict';
3+
/** Class representing a node in a tree structure, which each node has value and children(nodes) saving by key. */
4+
class Node {
5+
/**
6+
* Create a node.
7+
* @param value - The value of the node.
8+
*/
9+
constructor(value){
10+
this.value = value;
11+
this.childrenAsKeyValue = {};
12+
}
13+
/**
14+
* Add child to the node.
15+
* @param node - The node which going to be the child.
16+
* @param key - The key which is the identifier of the child.
17+
*/
18+
addChild(node, key){
19+
this.childrenAsKeyValue[key] = node;
20+
}
21+
/**
22+
* Override node data by other node by reference.
23+
* @param node - The node which going to use to take his data.
24+
*/
25+
setData(node){
26+
if (node instanceof Node){
27+
this.value = node.value;
28+
this.childrenAsKeyValue = node.childrenAsKeyValue;
29+
}
30+
};
31+
/**
32+
* Get node value.
33+
* @return The value of the node.
34+
*/
35+
getValue(){
36+
return this.value;
37+
}
38+
}
39+
40+
module.exports = {Node};

src/middleware.js

Lines changed: 22 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
'use strict';
22

33
var SwaggerParser = require('swagger-parser'),
4-
Ajv = require('ajv'),
5-
Validators = require('./utils/validators'),
6-
filesKeyword = require('./customKeywords/files'),
7-
contentKeyword = require('./customKeywords/contentTypeValidation'),
84
InputValidationError = require('./inputValidationError'),
95
schemaPreprocessor = require('./utils/schema-preprocessor'),
6+
swagger3 = require('./swagger3/open-api3'),
7+
swagger2 = require('./swagger2'),
8+
ajvUtils = require('./utils/ajv-utils'),
9+
Ajv = require('ajv'),
1010
sourceResolver = require('./utils/sourceResolver');
11-
1211
var schemas = {};
1312
var middlewareOptions;
14-
var ajvConfigBody;
15-
var ajvConfigParams;
1613
var framework;
1714

1815
/**
@@ -22,8 +19,6 @@ var framework;
2219
*/
2320
function init(swaggerPath, options) {
2421
middlewareOptions = options || {};
25-
ajvConfigBody = middlewareOptions.ajvConfigBody || {};
26-
ajvConfigParams = middlewareOptions.ajvConfigParams || {};
2722
framework = middlewareOptions.framework ? require(`./frameworks/${middlewareOptions.framework}`) : require('./frameworks/express');
2823
const makeOptionalAttributesNullable = middlewareOptions.makeOptionalAttributesNullable || false;
2924

@@ -39,19 +34,21 @@ function init(swaggerPath, options) {
3934
Object.keys(dereferenced.paths[currentPath]).filter(function (parameter) { return parameter !== 'parameters' })
4035
.forEach(function (currentMethod) {
4136
schemas[parsedPath][currentMethod.toLowerCase()] = {};
42-
37+
const isOpenApi3 = dereferenced.openapi === '3.0.0';
4338
const parameters = dereferenced.paths[currentPath][currentMethod].parameters || [];
44-
45-
let bodySchema = middlewareOptions.expectFormFieldsInBody
46-
? parameters.filter(function (parameter) { return (parameter.in === 'body' || (parameter.in === 'formData' && parameter.type !== 'file')) })
47-
: parameters.filter(function (parameter) { return parameter.in === 'body' });
48-
49-
if (makeOptionalAttributesNullable) {
50-
schemaPreprocessor.makeOptionalAttributesNullable(bodySchema);
51-
}
52-
if (bodySchema.length > 0) {
53-
const validatedBodySchema = _getValidatedBodySchema(bodySchema);
54-
schemas[parsedPath][currentMethod].body = buildBodyValidation(validatedBodySchema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath);
39+
if (isOpenApi3){
40+
schemas[parsedPath][currentMethod].body = swagger3.buildBodyValidation(dereferenced, swaggers[1], currentPath, currentMethod, middlewareOptions);
41+
} else {
42+
let bodySchema = middlewareOptions.expectFormFieldsInBody
43+
? parameters.filter(function (parameter) { return (parameter.in === 'body' || (parameter.in === 'formData' && parameter.type !== 'file')) })
44+
: parameters.filter(function (parameter) { return parameter.in === 'body' });
45+
if (makeOptionalAttributesNullable) {
46+
schemaPreprocessor.makeOptionalAttributesNullable(bodySchema);
47+
}
48+
if (bodySchema.length > 0) {
49+
const validatedBodySchema = swagger2.getValidatedBodySchema(bodySchema);
50+
schemas[parsedPath][currentMethod].body = swagger2.buildBodyValidation(validatedBodySchema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath, middlewareOptions);
51+
}
5552
}
5653

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

6158
if (localParameters.length > 0 || middlewareOptions.contentTypeValidation) {
6259
schemas[parsedPath][currentMethod].parameters = buildParametersValidation(localParameters,
63-
dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes);
60+
dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes, middlewareOptions);
6461
}
6562
});
6663
});
@@ -69,32 +66,6 @@ function init(swaggerPath, options) {
6966
return Promise.reject(error);
7067
});
7168
}
72-
73-
function _getValidatedBodySchema(bodySchema) {
74-
let validatedBodySchema;
75-
if (bodySchema[0].in === 'body') {
76-
// if we are processing schema for a simple JSON payload, no additional processing needed
77-
validatedBodySchema = bodySchema[0].schema;
78-
} else if (bodySchema[0].in === 'formData') {
79-
// if we are processing multipart form, assemble body schema from form field schemas
80-
validatedBodySchema = {
81-
required: [],
82-
properties: {}
83-
};
84-
bodySchema.forEach((formField) => {
85-
if (formField.type !== 'file') {
86-
validatedBodySchema.properties[formField.name] = {
87-
type: formField.type
88-
};
89-
if (formField.required) {
90-
validatedBodySchema.required.push(formField.name);
91-
}
92-
}
93-
});
94-
}
95-
return validatedBodySchema;
96-
}
97-
9869
/**
9970
* The middleware - should be called for each express route
10071
* @param {any} req
@@ -141,55 +112,6 @@ function _validateParams(headers, pathParams, query, files, path, method) {
141112
});
142113
}
143114

144-
function addCustomKeyword(ajv, formats) {
145-
if (formats) {
146-
formats.forEach(function (format) {
147-
ajv.addFormat(format.name, format.pattern);
148-
});
149-
}
150-
151-
ajv.addKeyword('files', filesKeyword);
152-
ajv.addKeyword('content', contentKeyword);
153-
}
154-
155-
function buildBodyValidation(schema, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath) {
156-
const defaultAjvOptions = {
157-
allErrors: true
158-
// unknownFormats: 'ignore'
159-
};
160-
const options = Object.assign({}, defaultAjvOptions, ajvConfigBody);
161-
let ajv = new Ajv(options);
162-
163-
addCustomKeyword(ajv, middlewareOptions.formats);
164-
165-
if (schema.discriminator) {
166-
return buildInheritance(schema.discriminator, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, ajv);
167-
} else {
168-
return new Validators.SimpleValidator(ajv.compile(schema));
169-
}
170-
}
171-
172-
function buildInheritance(discriminator, dereferencedDefinitions, swagger, currentPath, currentMethod, parsedPath, ajv) {
173-
let bodySchema = swagger.paths[currentPath][currentMethod].parameters.filter(function (parameter) { return parameter.in === 'body' })[0];
174-
var inheritsObject = {
175-
inheritance: []
176-
};
177-
inheritsObject.discriminator = discriminator;
178-
179-
Object.keys(swagger.definitions).forEach(key => {
180-
if (swagger.definitions[key].allOf) {
181-
swagger.definitions[key].allOf.forEach(element => {
182-
if (element['$ref'] && element['$ref'] === bodySchema.schema['$ref']) {
183-
inheritsObject[key] = ajv.compile(dereferencedDefinitions[key]);
184-
inheritsObject.inheritance.push(key);
185-
}
186-
});
187-
}
188-
}, this);
189-
190-
return new Validators.OneOfValidator(inheritsObject);
191-
}
192-
193115
function createContentTypeHeaders(validate, contentTypes) {
194116
if (!validate || !contentTypes) return;
195117

@@ -198,16 +120,16 @@ function createContentTypeHeaders(validate, contentTypes) {
198120
};
199121
}
200122

201-
function buildParametersValidation(parameters, contentTypes) {
123+
function buildParametersValidation(parameters, contentTypes, middlewareOptions) {
202124
const defaultAjvOptions = {
203125
allErrors: true,
204126
coerceTypes: 'array'
205127
// unknownFormats: 'ignore'
206128
};
207-
const options = Object.assign({}, defaultAjvOptions, ajvConfigParams);
129+
const options = Object.assign({}, defaultAjvOptions, middlewareOptions.ajvConfigParams);
208130
let ajv = new Ajv(options);
209131

210-
addCustomKeyword(ajv, middlewareOptions.formats);
132+
ajvUtils.addCustomKeyword(ajv, middlewareOptions.formats);
211133

212134
var ajvParametersSchema = {
213135
title: 'HTTP parameters',

src/swagger2/index.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
2+
const Validators = require('../validators'),
3+
Ajv = require('ajv'),
4+
ajvUtils = require('../utils/ajv-utils');
5+
6+
module.exports = {
7+
getValidatedBodySchema,
8+
buildBodyValidation
9+
};
10+
11+
function getValidatedBodySchema(bodySchema) {
12+
let validatedBodySchema;
13+
if (bodySchema[0].in === 'body') {
14+
// if we are processing schema for a simple JSON payload, no additional processing needed
15+
validatedBodySchema = bodySchema[0].schema;
16+
} else if (bodySchema[0].in === 'formData') {
17+
// if we are processing multipart form, assemble body schema from form field schemas
18+
validatedBodySchema = {
19+
required: [],
20+
properties: {}
21+
};
22+
bodySchema.forEach((formField) => {
23+
if (formField.type !== 'file') {
24+
validatedBodySchema.properties[formField.name] = {
25+
type: formField.type
26+
};
27+
if (formField.required) {
28+
validatedBodySchema.required.push(formField.name);
29+
}
30+
}
31+
});
32+
}
33+
return validatedBodySchema;
34+
}
35+
36+
function buildBodyValidation(schema, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, middlewareOptions = {}) {
37+
const defaultAjvOptions = {
38+
allErrors: true
39+
// unknownFormats: 'ignore'
40+
};
41+
const options = Object.assign({}, defaultAjvOptions, middlewareOptions.ajvConfigBody);
42+
let ajv = new Ajv(options);
43+
44+
ajvUtils.addCustomKeyword(ajv, middlewareOptions.formats);
45+
46+
if (schema.discriminator) {
47+
return buildInheritance(schema.discriminator, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, ajv);
48+
} else {
49+
return new Validators.SimpleValidator(ajv.compile(schema));
50+
}
51+
}
52+
53+
function buildInheritance(discriminator, dereferencedDefinitions, swagger, currentPath, currentMethod, parsedPath, ajv) {
54+
let bodySchema = swagger.paths[currentPath][currentMethod].parameters.filter(function (parameter) { return parameter.in === 'body' })[0];
55+
var inheritsObject = {
56+
inheritance: []
57+
};
58+
inheritsObject.discriminator = discriminator;
59+
60+
Object.keys(swagger.definitions).forEach(key => {
61+
if (swagger.definitions[key].allOf) {
62+
swagger.definitions[key].allOf.forEach(element => {
63+
if (element['$ref'] && element['$ref'] === bodySchema.schema['$ref']) {
64+
inheritsObject[key] = ajv.compile(dereferencedDefinitions[key]);
65+
inheritsObject.inheritance.push(key);
66+
}
67+
});
68+
}
69+
}, this);
70+
71+
return new Validators.OneOfValidator(inheritsObject);
72+
}

0 commit comments

Comments
 (0)