swagger-tools
Version:
Various tools for using and integrating with Swagger.
433 lines (354 loc) • 14 kB
JavaScript
/*
* The MIT License (MIT)
*
* Copyright (c) 2014 Apigee Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
;
var _ = require('lodash');
var cHelpers = require('../lib/helpers');
var debug = require('debug')('swagger-tools:middleware:router');
var fs = require('fs');
var mHelpers = require('./helpers');
var path = require('path');
var defaultOptions = {
controllers: {},
useStubs: false // Should we set this automatically based on process.env.NODE_ENV?
};
var getHandlerName = function (req) {
var handlerName;
switch (req.swagger.swaggerVersion) {
case '1.2':
handlerName = req.swagger.operation.nickname;
break;
case '2.0':
if (req.swagger.operation['x-swagger-router-controller'] || req.swagger.path['x-swagger-router-controller']) {
handlerName = (req.swagger.operation['x-swagger-router-controller'] ?
req.swagger.operation['x-swagger-router-controller'] :
req.swagger.path['x-swagger-router-controller']) + '_' +
(req.swagger.operation.operationId ? req.swagger.operation.operationId : req.method.toLowerCase());
} else {
handlerName = req.swagger.operation.operationId;
}
break;
}
return handlerName;
};
var handlerCacheFromDir = function (dirOrDirs) {
var handlerCache = {};
var jsFileRegex = /\.(coffee|js|ts)$/;
var dirs = [];
if (_.isArray(dirOrDirs)) {
dirs = dirOrDirs;
} else {
dirs.push(dirOrDirs);
}
debug(' Controllers:');
_.each(dirs, function (dir) {
_.each(fs.readdirSync(dir), function (file) {
var controllerName = file.replace(jsFileRegex, '');
var controller;
if (file.match(jsFileRegex)) {
controller = require(path.resolve(path.join(dir, controllerName)));
debug(' %s%s:',
path.resolve(path.join(dir, file)),
(_.isPlainObject(controller) ? '' : ' (not an object, skipped)'));
if (_.isPlainObject(controller)) {
_.each(controller, function (value, name) {
var handlerId = controllerName + '_' + name;
debug(' %s%s',
handlerId,
(_.isFunction(value) ? '' : ' (not a function, skipped)'));
// TODO: Log this situation
if (_.isFunction(value) && !handlerCache[handlerId]) {
handlerCache[handlerId] = value;
}
});
}
}
});
});
return handlerCache;
};
var getMockValue = function (version, schema) {
var type = _.isPlainObject(schema) ? schema.type : schema;
var value;
if (!type) {
type = 'object';
}
switch (type) {
case 'array':
value = [getMockValue(version, _.isArray(schema.items) ? schema.items[0] : schema.items)];
break;
case 'boolean':
if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
value = schema.defaultValue;
} else if (version === '2.0' && !_.isUndefined(schema.default)) {
value = schema.default;
} else if (_.isArray(schema.enum)) {
value = schema.enum[0];
} else {
value = 'true';
}
// Convert value if necessary
value = value === 'true' || value === true ? true : false;
break;
case 'file':
case 'File':
value = 'Pretend this is some file content';
break;
case 'integer':
if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
value = schema.defaultValue;
} else if (version === '2.0' && !_.isUndefined(schema.default)) {
value = schema.default;
} else if (_.isArray(schema.enum)) {
value = schema.enum[0];
} else {
value = 1;
}
// Convert value if necessary
if (!_.isNumber(value)) {
value = parseInt(value, 10);
}
// TODO: Handle constraints and formats
break;
case 'object':
value = {};
_.each(schema.allOf, function (parentSchema) {
_.each(parentSchema.properties, function (property, propName) {
value[propName] = getMockValue(version, property);
});
});
_.each(schema.properties, function (property, propName) {
value[propName] = getMockValue(version, property);
});
break;
case 'number':
if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
value = schema.defaultValue;
} else if (version === '2.0' && !_.isUndefined(schema.default)) {
value = schema.default;
} else if (_.isArray(schema.enum)) {
value = schema.enum[0];
} else {
value = 1.0;
}
// Convert value if necessary
if (!_.isNumber(value)) {
value = parseFloat(value);
}
// TODO: Handle constraints and formats
break;
case 'string':
if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
value = schema.defaultValue;
} else if (version === '2.0' && !_.isUndefined(schema.default)) {
value = schema.default;
} else if (_.isArray(schema.enum)) {
value = schema.enum[0];
} else {
if (schema.format === 'date') {
value = new Date().toISOString().split('T')[0];
} else if (schema.format === 'date-time') {
value = new Date().toISOString();
} else {
value = 'Sample text';
}
}
break;
}
return value;
};
var mockResponse = function (req, res, next, handlerName) {
var method = req.method.toLowerCase();
var operation = req.swagger.operation;
var sendResponse = function (err, response) {
if (err) {
debug('next with error: %j', err);
return next(err);
} else {
debug('send mock response: %s', response);
// Explicitly set the response status to 200 if not present (Issue #269)
if (_.isUndefined(req.statusCode)) {
res.statusCode = 200;
}
// Mock mode only supports JSON right now
res.setHeader('Content-Type', 'application/json');
return res.end(response);
}
};
var spec = cHelpers.getSpec(req.swagger.swaggerVersion);
var stubResponse = 'Stubbed response for ' + handlerName;
var apiDOrSO;
var responseType;
switch (req.swagger.swaggerVersion) {
case '1.2':
apiDOrSO = req.swagger.apiDeclaration;
responseType = operation.type;
break;
case '2.0':
apiDOrSO = req.swagger.swaggerObject;
if (method === 'post' && operation.responses['201']) {
responseType = operation.responses['201'];
res.statusCode = 201;
} else if (method === 'delete' && operation.responses['204']) {
responseType = operation.responses['204'];
res.statusCode = 204;
} else if (operation.responses['200']) {
responseType = operation.responses['200'];
} else if (operation.responses['default']) {
responseType = operation.responses['default'];
} else {
responseType = 'void';
}
break;
}
if (_.isPlainObject(responseType) || mHelpers.isModelType(spec, responseType)) {
if (req.swagger.swaggerVersion === '1.2') {
spec.composeModel(apiDOrSO, responseType, function (err, result) {
if (err) {
return sendResponse(undefined, err);
} else {
// Should we handle this differently as undefined typically means the model doesn't exist
return sendResponse(undefined, _.isUndefined(result) ?
stubResponse :
JSON.stringify(getMockValue(req.swagger.swaggerVersion, result)));
}
});
} else {
return sendResponse(undefined, JSON.stringify(getMockValue(req.swagger.swaggerVersion, responseType.schema || responseType)));
}
} else {
return sendResponse(undefined, getMockValue(req.swagger.swaggerVersion, responseType));
}
};
var createStubHandler = function (req, res, next, handlerName) {
// TODO: Handle headers for 2.0
// TODO: Handle examples (per mime-type) for 2.0
// TODO: Handle non-JSON response types
return function stubHandler (req, res, next) {
mockResponse(req, res, next, handlerName);
};
};
var send405 = function (req, res, next) {
var allowedMethods = [];
var err = new Error('Route defined in Swagger specification (' +
(_.isUndefined(req.swagger.api) ? req.swagger.apiPath : req.swagger.api.path) +
') but there is no defined ' +
(req.swagger.swaggerVersion === '1.2' ? req.method.toUpperCase() : req.method.toLowerCase()) + ' operation.');
if (!_.isUndefined(req.swagger.api)) {
_.each(req.swagger.api.operations, function (operation) {
allowedMethods.push(operation.method.toUpperCase());
});
} else {
_.each(req.swagger.path, function (operation, method) {
if (cHelpers.swaggerOperationMethods.indexOf(method.toUpperCase()) !== -1) {
allowedMethods.push(method.toUpperCase());
}
});
}
err.allowedMethods = allowedMethods;
res.setHeader('Allow', allowedMethods.sort().join(', '));
res.statusCode = 405;
return next(err);
};
/**
* Middleware for using Swagger information to route requests to handlers. Due to the differences between Swagger 1.2
* and Swagger 2.0, the way in which your Swagger document(s) are annotated to work with this middleware differs as well
* so please view the documentation below for more details:
*
* https://github.com/apigee-127/swagger-tools/blob/master/docs/Middleware.md#swaggerrouteroptions
*
* This middleware also requires that you use the swagger-metadata middleware before this middleware. This middleware
* also makes no attempt to work around invalid Swagger documents. If you would like to validate your requests using
* the swagger-validator middleware, you must use it prior to using this middleware.
*
* @param {object} [options] - The middleware options
* @param {(string|object|string[]} [options.controllers=./controllers] - If this is a string or string array, this is
* the path, or paths, to find the controllers
* in. If it's an object, the keys are the
* controller "name" (as described above) and the
* value is a function.
* @param {boolean} [options.useStubs=false] - Whether or not to stub missing controllers and methods
*
* @returns the middleware function
*/
exports = module.exports = function (options) {
var handlerCache = {};
debug('Initializing swagger-router middleware');
// Set the defaults
options = _.defaults(options || {}, defaultOptions);
debug(' Mock mode: %s', options.useStubs === true ? 'enabled' : 'disabled');
if (_.isPlainObject(options.controllers)) {
debug(' Controllers:');
// Create the handler cache from the passed in controllers object
_.each(options.controllers, function (func, handlerName) {
debug(' %s', handlerName);
if (!_.isFunction(func)) {
throw new Error('options.controllers values must be functions');
}
});
handlerCache = options.controllers;
} else {
// Create the handler cache from the modules in the controllers directory
handlerCache = handlerCacheFromDir(options.controllers);
}
return function swaggerRouter (req, res, next) {
var operation = req.swagger ? req.swagger.operation : undefined;
var handler;
var handlerName;
var rErr;
debug('%s %s', req.method, req.url);
debug(' Will process: %s', _.isUndefined(operation) ? 'no' : 'yes');
if (req.swagger) {
if (operation) {
handlerName = getHandlerName(req);
handler = handlerCache[handlerName];
req.swagger.useStubs = options.useStubs;
debug(' Route handler: %s', handlerName);
debug(' Missing: %s', _.isUndefined(handler) ? 'yes' : 'no');
debug(' Ignored: %s', options.ignoreMissingHandlers === true ? 'yes' : 'no');
debug(' Using mock: %s', options.useStubs && _.isUndefined(handler) ? 'yes' : 'no');
if (_.isUndefined(handler) && options.useStubs === true) {
handler = handlerCache[handlerName] = createStubHandler(handlerName);
}
if (!_.isUndefined(handler)) {
try {
return handler(req, res, next);
} catch (err) {
rErr = err;
debug('Handler threw an unexpected error: %s\n%s', err.message, err.stack);
}
} else if (options.ignoreMissingHandlers !== true) {
rErr = new Error('Cannot resolve the configured swagger-router handler: ' + handlerName);
res.statusCode = 500;
}
} else {
debug(' No handler for method: %s', req.method);
return send405(req, res, next);
}
}
if (rErr) {
mHelpers.debugError(rErr, debug);
}
return next(rErr);
};
};