Skip to content

Commit

Permalink
Update spec to OpenAPI 3.0.1
Browse files Browse the repository at this point in the history
- `swagger: 2.0` has been replaced with `openapi: 3.0.1`
- `definitions` was renamed to `schemas` and 
  `securityDefinitions` to `securitySchemes` and they all were
  moved inside components
- servers replaces the basePath  keywords used in OpenAPI 2.0

Bug: T218218
  • Loading branch information
clarakosi committed Apr 23, 2019
1 parent f974df2 commit 2cdf488
Show file tree
Hide file tree
Showing 13 changed files with 310 additions and 187 deletions.
134 changes: 69 additions & 65 deletions lib/filters/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const inMapping = {
path: 'params',
query: 'query',
header: 'headers',
formData: 'body',
body: 'body'
};

Expand Down Expand Up @@ -43,44 +42,44 @@ const supportedValidators = ['maximum',
* @constructor
*/
class Validator {
constructor(parameters, definitions) {
constructor(parameters = [], requestBody = {}, schemas) {
this._ajv = constructAjv({ verbose: true });
if (definitions) {
Object.keys(definitions).forEach((schemaName) => {
if (schemas) {
Object.keys(schemas).forEach((schemaName) => {
this._ajv.addSchema(
definitions[schemaName],
`#/definitions/${schemaName}`
schemas[schemaName],
`#/components/schemas/${schemaName}`
);
});
}

this._paramCoercionFunc = this._createTypeCoercionFunc(parameters.filter((p) => {
return p.in !== 'formData' && p.in !== 'body' && p.type !== 'string';
return p.schema.type !== 'string';
}));
const bodyParams = parameters.filter((p) => p.in === 'body');
if (bodyParams.length > 1) {
throw new Error('Only a single body parameter allowed');
} else if (bodyParams.length) {
// Have a body parameter, special-case coercion to support formData and JSON
const bodyParam = bodyParams[0];
if (bodyParam.schema && bodyParam.schema.properties) {

if (requestBody.content) {
// Have a body parameter, special-case coercion to support form-data and JSON
const reqContent = requestBody.content;
const bodyType = Object.keys(reqContent)[0];

if (reqContent[bodyType].schema && reqContent[bodyType].schema.properties) {
this._bodyCoercionFunc = this._createTypeCoercionFunc(
Object.keys(bodyParam.schema.properties).map((prop) => {
return {
name: prop,
in: 'body',
type: bodyParam.schema.properties[prop].type,
required: bodyParam.schema.required &&
bodyParam.schema.required.indexOf(prop) !== -1
};
}));
Object.keys(reqContent[bodyType].schema.properties)
.map((prop) => {
return {
name: prop,
in: 'body',
schema: {
type: reqContent[bodyType].schema.properties[prop].type
},
required: reqContent[bodyType].schema.required &&
reqContent[bodyType].schema.required.indexOf(prop) !== -1
};
})
);
}
} else {
this._bodyCoercionFunc = this._createTypeCoercionFunc(parameters.filter((p) => {
return p.in === 'formData' && p.type !== 'string';
}));
}
this._validatorFunc = this._ajv.compile(this._convertToJsonSchema(parameters));
this._validatorFunc = this._ajv.compile(this._convertToJsonSchema(parameters, requestBody));
}

/**
Expand Down Expand Up @@ -124,53 +123,57 @@ class Validator {
* Converts a list of parameters from a swagger spec
* to JSON-schema for a request
* @param {Array} parameters list of params
* @param {Object} requestBody schema
* @return {Object} JSON schema
* @private
*/
Validator.prototype._convertToJsonSchema = function (parameters) {
Validator.prototype._convertToJsonSchema = function (parameters, requestBody) {
const schema = {
type: 'object',
properties: {}
};

parameters.forEach((param) => {
if (param.in !== 'body') {
if (!schema.properties[inMapping[param.in]]) {
schema.properties[inMapping[param.in]] = {
type: 'object',
properties: {}
};
// 'required' array must have at least one element according to json-schema spec,
// se we can't preinitialize it.
schema.required = schema.required || [];
schema.required.push(inMapping[param.in]);
}
if (!schema.properties[inMapping[param.in]]) {
schema.properties[inMapping[param.in]] = {
type: 'object',
properties: {}
};
// 'required' array must have at least one element according to json-schema spec,
// se we can't preinitialize it.
schema.required = schema.required || [];
schema.required.push(inMapping[param.in]);
}

const reqPartSchema = schema.properties[inMapping[param.in]];
const paramSchema = { type: param.type };
supportedValidators.forEach((validator) => {
paramSchema[validator] = param[validator];
});
reqPartSchema.properties[param.name] = paramSchema;
if (param.required) {
reqPartSchema.required = reqPartSchema.required || [];
reqPartSchema.required.push(param.name);
}
} else {
if (param.schema) {
schema.properties.body = param.schema;
} else {
schema.properties.body = {
type: 'object'
};
}
if (param.required) {
schema.required = schema.required || [];
schema.required.push('body');
}
const reqPartSchema = schema.properties[inMapping[param.in]];
const paramSchema = { type: param.schema.type };

supportedValidators.forEach((validator) => {
paramSchema[validator] = param.schema[validator];
});
reqPartSchema.properties[param.name] = paramSchema;
if (param.required) {
reqPartSchema.required = reqPartSchema.required || [];
reqPartSchema.required.push(param.name);
}
});

if (requestBody.content) {
const bodyType = Object.keys(requestBody.content)[0];

if (requestBody.content[bodyType].schema) {
schema.properties.body = requestBody.content[bodyType].schema;
} else {
schema.properties.body = {
type: 'object'
};
}
if (requestBody.required) {
schema.required = schema.required || [];
schema.required.push('body');
}
}

return schema;
};

Expand All @@ -187,7 +190,7 @@ Validator.prototype._createTypeCoercionFunc = function (parameters) {
const paramAccessor = `req.${inMapping[param.in]}["${param.name}"]`;
let paramCoercionCode = '';
let errorNotifier;
switch (param.type) {
switch (param.schema.type) {
case 'integer':
errorNotifier = `${'throw new HTTPError({status:400,body:{type:"bad_request",' +
' title:"Invalid parameters", detail: "data.'}${
Expand Down Expand Up @@ -249,14 +252,15 @@ Validator.prototype._createTypeCoercionFunc = function (parameters) {
const CACHE = new Map();

module.exports = (hyper, req, next, options, specInfo) => {
if (specInfo && specInfo.spec && specInfo.spec.parameters) {
if (specInfo && specInfo.spec && (specInfo.spec.parameters || specInfo.spec.requestBody)) {
const cachedValidator = CACHE.get(specInfo.spec);
if (cachedValidator) {
cachedValidator.validate(req);
} else {
const validator = new Validator(
specInfo.spec.parameters,
specInfo.specRoot && specInfo.specRoot.definitions
specInfo.spec.requestBody,
specInfo.specRoot.components && specInfo.specRoot.components.schemas
);
CACHE.set(specInfo.spec, validator);
validator.validate(req);
Expand Down
2 changes: 1 addition & 1 deletion lib/hyperswitch.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ HyperSwitch.prototype.defaultListingHandler = function (match, hyper, req) {
status: 200,
body: Object.assign({}, match.value.specRoot, {
// Set the base path dynamically
basePath: getDocBasePath(req, match.value.specRoot)
servers: [{ url: getDocBasePath(req, match.value.specRoot) }]
})
});
} else if (rq.path ||
Expand Down
19 changes: 9 additions & 10 deletions lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const fs = P.promisifyAll(require('fs'));
const handlerTemplate = require('./handlerTemplate');
const swaggerRouter = require('swagger-router');
const path = require('path');
const utils = require('./utils');

const Node = swaggerRouter.Node;
const Template = swaggerRouter.Template;
Expand Down Expand Up @@ -440,7 +441,7 @@ class Router {
subtree = new Node();
// Set up our specific value object
subtree.value = value;
value.path = childScope.specRoot.basePath + childScope.prefixPath;
value.path = childScope.specRoot.servers[0].url + childScope.prefixPath;
value.methods = {};
// XXX: Set ACLs and other value properties for path
// subtree.value.acls = ...;
Expand Down Expand Up @@ -474,7 +475,7 @@ class Router {
subtree.value = value;
// Copy over the remaining value properties.
Object.assign(subtree.value, origSubtree.value);
subtree.value.path = childScope.specRoot.basePath + childScope.prefixPath;
subtree.value.path = childScope.specRoot.servers[0].url + childScope.prefixPath;
specPromise = P.resolve();
}
branchNode.setChild(path[path.length - 1], subtree);
Expand Down Expand Up @@ -505,11 +506,10 @@ class Router {
scope.rootScope = scope;
}

// Merge in definitions & securityDefinitions from the spec.
// Merge in components from the spec.
// TODO: Do we need a clone here? Is it okay if those definitions are
// added to the higher level spec?
Object.assign(scope.specRoot.definitions, spec.definitions);
Object.assign(scope.specRoot.securityDefinitions, spec.securityDefinitions);
utils.mergeDeep(scope.specRoot.components, spec.components);
scope.specRoot.tags = scope.specRoot.tags.concat(spec.tags || [])
.filter((tag, index, self) => {
return index === self.findIndex((t) => {
Expand Down Expand Up @@ -637,22 +637,21 @@ class Router {
Router.prototype._createNewApiRoot = function (node, spec, scope) {
const specRoot = Object.assign({}, spec);
// Make sure the spec has the standard properties set up.
specRoot.swagger = spec.swagger || '2.0';
specRoot.definitions = spec.definitions || {};
specRoot.securityDefinitions = spec.securityDefinitions || {};
specRoot.openapi = spec.openapi || '3.0.1';
specRoot.components = spec.components || {};
specRoot['x-default-params'] = spec['x-default-params'] || {};
specRoot.tags = spec.tags || [];

delete specRoot['x-route-filters'];

// Reset paths. These are going to be built up during path setup.
specRoot.paths = {};
specRoot.basePath = scope.prefixPath;
specRoot.servers = [{ url: scope.prefixPath }];

node.setChild({ type: 'meta', name: 'apiRoot' }, new Node({
specRoot,
methods: {},
path: `${specRoot.basePath}/`,
path: `${specRoot.servers[0].url}/`,
globals: node.value && node.value.globals || scope.globals
}));

Expand Down
32 changes: 32 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,36 @@ class NullLogger {
}
utils.nullLogger = new NullLogger();

/**
* Deep merge two objects.
* @param {Object} target
* @param {Object} ...sources
* @return {Object}
*/
utils.mergeDeep = (target, ...sources) => {
const isObject = (item) => {
return (item && typeof item === 'object' && !Array.isArray(item));
};

if (!sources.length || !sources || !target || !isObject(target)) {
return target;
}

sources.forEach((source) => {
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach((key) => {
if (isObject(source[key])) {
if (!target[key]) {
target[key] = {};
}
utils.mergeDeep(target[key], source[key]);
} else {
target[key] = source[key];
}
});
}
});
return target;
};

module.exports = utils;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hyperswitch",
"version": "0.11.1",
"version": "0.12.0",
"description": "REST API creation framework",
"main": "index.js",
"scripts": {
Expand Down
Loading

0 comments on commit 2cdf488

Please sign in to comment.