Skip to content

Commit

Permalink
Adding vendor extensions to configure middleware.
Browse files Browse the repository at this point in the history
* Middleware may currently be disabled.
* All extensions are scoped.
* The following extensions have been added:
  * x-express-openapi-disable-middleware
  * x-express-openapi-disable-coercion-middleware
  * x-express-openapi-disable-defaults-middleware
  * x-express-openapi-disable-response-validation-middleware
  * x-express-openapi-disable-validation-middleware
  • Loading branch information
jsdevel committed Feb 4, 2016
1 parent e1a63c0 commit 4d24605
Show file tree
Hide file tree
Showing 63 changed files with 3,326 additions and 22 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and validation.
* See [express-openapi-response-validation](https://github.com/kogosoftwarellc/express-openapi-response-validation)
* Validates api documents.
* See [openapi-schema-validation](https://github.com/kogosoftwarellc/openapi-schema-validation)
* Configurable Middleware.
* See [Configuring Middleware](#configuring-middleware)
* Performant.
* Extensively tested.
* Small footprint.
Expand Down Expand Up @@ -62,6 +64,28 @@ Our routes are now active and we can test them out with Swagger UI:

For more examples see the [sample projects](https://github.com/kogosoftwarellc/express-openapi/tree/master/test/sample-projects) used in tests.

## Configuring Middleware

You can directly control what middleware `express-openapi` adds to your express app
by using the following vendor extension properties. These properties are scoped, so
if you use one as a root property of your API Document, all paths and operations will be affected. Similarly if you just want to disable middleware for an operation, you can
use these properties in said operation's apiDoc. See full examples in the
[./test/sample-projects/](
https://github.com/kogosoftwarellc/express-openapi/tree/master/test/sample-projects)
directory.

### Supported vendor extensions

* `'x-express-openapi-disable-middleware': true` - Disables all middleware.
* `'x-express-openapi-disable-coercion-middleware': true` - Disables coercion middleware.
* `'x-express-openapi-disable-defaults-middleware': true` - Disables
defaults middleware.
* `'x-express-openapi-disable-response-validation-middleware': true` - Disables
response validation middleware I.E. no `res.validateResponse` method will be
available in the affected operation handler method.
* `'x-express-openapi-disable-validation-middleware': true` - Disables input
validation middleware.

## API

### .initialize(args)
Expand Down
79 changes: 57 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,28 +68,30 @@ function initialize(args) {
var errorTransformer = args.errorTransformer;

fsRoutes(routesDir).forEach(function(result) {
var routeModule = require(result.path);
var pathModule = require(result.path);
var route = result.route;
// express path pargumentarams start with :paramName
// openapi path params use {paramName}
var openapiPath = route;
var pathItem = apiDoc.paths[openapiPath] || {};
var pathParameters = Array.isArray(routeModule.parameters) ?
[].concat(routeModule.parameters) :
var pathParameters = Array.isArray(pathModule.parameters) ?
[].concat(pathModule.parameters) :
[];
pathItem.parameters = pathParameters;
apiDoc.paths[openapiPath] = pathItem;

Object.keys(routeModule).filter(byMethods).forEach(function(methodName) {
Object.keys(pathModule).filter(byMethods).forEach(function(methodName) {
// methodHandler may be an array or a function.
var methodHandler = routeModule[methodName];
var methodHandler = pathModule[methodName];
var methodDoc = methodHandler.apiDoc;
var middleware = [].concat(methodHandler);

if (methodDoc) {
if (methodDoc &&
allowsMiddleware(apiDoc, pathModule, pathItem, methodDoc)) {// add middleware
pathItem[methodName] = copy(methodDoc);

if (methodDoc.responses) {
if (methodDoc.responses && allowsResponseValidationMiddleware(apiDoc,
pathModule, pathItem, methodDoc)) {// add response validation middleware
// it's invalid for a method doc to not have responses, but the post
// validation will pick it up, so this is almost always going to be added.
middleware.unshift(buildResponseValidationMiddleware({
Expand All @@ -103,24 +105,25 @@ function initialize(args) {
withNoDuplicates(pathParameters.concat(methodDoc.parameters)) :
pathParameters;

if (methodParameters.length) {
var defaultsMiddleware;

// no point in default middleware if we don't have any parameters with defaults.
if (methodParameters.filter(byDefault).length) {
defaultsMiddleware = buildDefaultsMiddleware({parameters: methodParameters});
if (methodParameters.length) {// defaults, coercion, and parameter validation middleware
if (allowsValidationMiddleware(apiDoc, pathModule, pathItem, methodDoc)) {
var validationMiddleware = buildValidationMiddleware({
errorTransformer: errorTransformer,
parameters: methodParameters,
schemas: apiDoc.definitions
});
middleware.unshift(validationMiddleware);
}

var coercionMiddleware = buildCoercionMiddleware({parameters: methodParameters});
var validationMiddleware = buildValidationMiddleware({
errorTransformer: errorTransformer,
parameters: methodParameters,
schemas: apiDoc.definitions
});

middleware.unshift(coercionMiddleware, validationMiddleware);
if (allowsCoercionMiddleware(apiDoc, pathModule, pathItem, methodDoc)) {
var coercionMiddleware = buildCoercionMiddleware({parameters: methodParameters});
middleware.unshift(coercionMiddleware);
}

if (defaultsMiddleware) {
// no point in default middleware if we don't have any parameters with defaults.
if (methodParameters.filter(byDefault).length &&
allowsDefaultsMiddleware(apiDoc, pathModule, pathItem, methodDoc)) {
var defaultsMiddleware = buildDefaultsMiddleware({parameters: methodParameters});
middleware.unshift(defaultsMiddleware);
}
}
Expand Down Expand Up @@ -152,6 +155,32 @@ function initialize(args) {
}
}

function allows(args, prop, val) {
return ![].slice.call(args).filter(byProperty(prop, val))
.length;
}

function allowsMiddleware() {
return allows(arguments, 'x-express-openapi-disable-middleware', true);
}

function allowsCoercionMiddleware() {
return allows(arguments, 'x-express-openapi-disable-coercion-middleware', true);
}

function allowsDefaultsMiddleware() {
return allows(arguments, 'x-express-openapi-disable-defaults-middleware', true);
}

function allowsResponseValidationMiddleware() {
return allows(arguments, 'x-express-openapi-disable-response-validation-middleware',
true);
}

function allowsValidationMiddleware() {
return allows(arguments, 'x-express-openapi-disable-validation-middleware', true);
}

function byDefault(param) {
return param && 'default' in param;
}
Expand All @@ -162,6 +191,12 @@ function byMethods(name) {
.indexOf(name) > -1;
}

function byProperty(property, value) {
return function(obj) {
return obj && property in obj && obj[property] === value;
};
}

function copy(obj) {
return JSON.parse(JSON.stringify(obj));
}
Expand Down
96 changes: 96 additions & 0 deletions test/sample-projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,100 @@ describe(require('../package.json').name + 'sample-projects', function() {
.expect(404, done);
});
});

describe('disabling middleware', function() {
var coercionMissingBody = {
errors: [
{
errorCode: 'type.openapi.validation',
location: 'path',
message: 'instance.id is not of a type(s) integer',
path: 'id'
}
],
status: 400
};

[
// disable coercion
{name: 'with-coercion-middleware-disabled-in-methodDoc', url: '/v3/users/34?name=fred',
expectedStatus: 400, expectedBody: coercionMissingBody},
{name: 'with-coercion-middleware-disabled-in-pathItem', url: '/v3/users/34?name=fred',
expectedStatus: 400, expectedBody: coercionMissingBody},
{name: 'with-coercion-middleware-disabled-in-pathModule', url: '/v3/users/34?name=fred',
expectedStatus: 400, expectedBody: coercionMissingBody},
{name: 'with-coercion-middleware-disabled-in-the-apiDoc', url: '/v3/users/34?name=fred',
expectedStatus: 400, expectedBody: coercionMissingBody},

// disable defaults
{name: 'with-defaults-middleware-disabled-in-methodDoc', url: '/v3/users/34?name=fred',
expectedStatus: 200, expectedBody: {id: 34, name: 'fred'}},
{name: 'with-defaults-middleware-disabled-in-pathItem', url: '/v3/users/34?name=fred',
expectedStatus: 200, expectedBody: {id: 34, name: 'fred'}},
{name: 'with-defaults-middleware-disabled-in-pathModule', url: '/v3/users/34?name=fred',
expectedStatus: 200, expectedBody: {id: 34, name: 'fred'}},
{name: 'with-defaults-middleware-disabled-in-the-apiDoc', url: '/v3/users/34?name=fred',
expectedStatus: 200, expectedBody: {id: 34, name: 'fred'}},

// disable validation
{name: 'with-validation-middleware-disabled-in-methodDoc', url: '/v3/users/asdf?name=fred',
expectedStatus: 200, expectedBody: {age: 80, id: null, name: 'fred'}},
{name: 'with-validation-middleware-disabled-in-pathItem', url: '/v3/users/asdf?name=fred',
expectedStatus: 200, expectedBody: {age: 80, id: null, name: 'fred'}},
{name: 'with-validation-middleware-disabled-in-pathModule', url: '/v3/users/asdf?name=fred',
expectedStatus: 200, expectedBody: {age: 80, id: null, name: 'fred'}},
{name: 'with-validation-middleware-disabled-in-the-apiDoc', url: '/v3/users/asdf?name=fred',
expectedStatus: 200, expectedBody: {age: 80, id: null, name: 'fred'}},

// disable all
{name: 'with-middleware-disabled-in-methodDoc', url: '/v3/users/asdf?name=fred',
expectedStatus: 200, expectedBody: {id: 'asdf', name: 'fred'}},
{name: 'with-middleware-disabled-in-pathItem', url: '/v3/users/asdf?name=fred',
expectedStatus: 200, expectedBody: {id: 'asdf', name: 'fred'}},
{name: 'with-middleware-disabled-in-pathModule', url: '/v3/users/asdf?name=fred',
expectedStatus: 200, expectedBody: {id: 'asdf', name: 'fred'}},
{name: 'with-middleware-disabled-in-the-apiDoc', url: '/v3/users/asdf?name=fred',
expectedStatus: 200, expectedBody: {id: 'asdf', name: 'fred'}}
].forEach(function(test) {
describe(test.name, function() {
var app = require('./sample-projects/' + test.name + '/app.js');

it('should meet expectations', function(done) {
request(app)
.get(test.url)
.expect(test.expectedStatus)
.end(function(err, res) {
expect(res.body).to.eql(test.expectedBody);
done(err);
});
});
});
});

[
// disable response validation
{name: 'with-response-validation-middleware-disabled-in-methodDoc',
url: '/v3/users/34?name=fred', expectedStatus: 200, expectedBody: true},
{name: 'with-response-validation-middleware-disabled-in-pathItem',
url: '/v3/users/34?name=fred', expectedStatus: 200, expectedBody: true},
{name: 'with-response-validation-middleware-disabled-in-pathModule',
url: '/v3/users/34?name=fred', expectedStatus: 200, expectedBody: true},
{name: 'with-response-validation-middleware-disabled-in-the-apiDoc',
url: '/v3/users/34?name=fred', expectedStatus: 200, expectedBody: true}
].forEach(function(test) {
describe(test.name, function() {
var app = require('./sample-projects/' + test.name + '/app.js');

it('should not expose res.validateResponse in the app', function(done) {
request(app)
.get(test.url)
.expect(test.expectedStatus)
.end(function(err, res) {
expect(res.body).to.eql(test.expectedBody);
done(err);
});
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// args.apiDoc needs to be a js object. This file could be a json file, but we can't add
// comments in json files.
module.exports = {
swagger: '2.0',

// all routes will now have /v3 prefixed.
basePath: '/v3',

info: {
title: 'express-openapi sample project',
version: '3.0.0'
},

definitions: {
Error: {
additionalProperties: true
},
User: {
properties: {
name: {
type: 'string'
},
friends: {
type: 'array',
items: {
$ref: '#/definitions/User'
}
}
},
required: ['name']
}
},

// paths are derived from args.routes. These are filled in by fs-routes.
paths: {},

tags: [
{name: 'users'}
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
module.exports = {
// parameters for all operations in this path
parameters: [
{
name: 'id',
in: 'path',
type: 'string',
required: true,
description: 'Fred\'s age.'
}
],
// method handlers may just be the method handler...
get: get,
// or they may also be an array of middleware + the method handler. This allows
// for flexible middleware management. express-openapi middleware generated from
// the <path>.parameters + <methodHandler>.apiDoc.parameters is prepended to this
// array.
post: [function(req, res, next) {next();}, function(req, res) {
res.status(200).json({id: req.params.id});
}]
};

module.exports.post.apiDoc = {
description: 'Create a user.',
operationId: 'createUser',
tags: ['users'],
parameters: [
{
name: 'user',
in: 'body',
schema: {
$ref: '#/definitions/User'
}
}
],

responses: {
default: {
$ref: '#/definitions/Error'
}
}
};

function get(req, res) {
res.status(200).json({
id: req.params.id,
name: req.query.name,
age: req.query.age
});
}

get.apiDoc = {
'x-express-openapi-disable-coercion-middleware': true,
description: 'Retrieve a user.',
operationId: 'getUser',
tags: ['users'],
parameters: [
{
name: 'name',
in: 'query',
type: 'string',
pattern: '^fred$',
description: 'The name of this person. It may only be "fred".'
},
// showing that operation parameters override path parameters
{
name: 'id',
in: 'path',
type: 'integer',
required: true,
description: 'Fred\'s age.'
},
{
name: 'age',
in: 'query',
type: 'integer',
description: 'Fred\'s age.',
default: 80
}
],

responses: {
200: {
$ref: '#/definitions/User'
},

default: {
$ref: '#/definitions/Error'
}
}
};
Loading

0 comments on commit 4d24605

Please sign in to comment.