diff --git a/middleware/helpers.js b/middleware/helpers.js index 9c865ebd0d..eeb378b556 100644 --- a/middleware/helpers.js +++ b/middleware/helpers.js @@ -32,278 +32,280 @@ var validators = require('../lib/validators'); var parseurl = require('parseurl'); var qs = require('qs'); -var isModelType = module.exports.isModelType = function (spec, type) { - return spec.primitives.indexOf(type) === -1; +var isModelType = module.exports.isModelType = function(spec, type) { + return spec.primitives.indexOf(type) === -1; }; -var getParameterType = module.exports.getParameterType = function (schema) { - var type = schema.type; +var getParameterType = module.exports.getParameterType = function(schema) { + var type = schema.type; - if (!type && schema.schema) { - type = getParameterType(schema.schema); - } + if (!type && schema.schema) { + type = getParameterType(schema.schema); + } - if (!type) { - type = 'object'; - } + if (!type) { + type = 'object'; + } - return type; + return type; }; -var isModelParameter = module.exports.isModelParameter = function (version, param) { - var spec = helpers.getSpec(version); - var type = getParameterType(param); - var isModel = false; - - if (type === 'object' || isModelType(spec, type)) { - isModel = true; - } else if (type === 'array' && isModelType(spec, param.items ? - param.items.type || param.items.$ref : - undefined)) { - isModel = true; - } +var isModelParameter = module.exports.isModelParameter = function(version, param) { + var spec = helpers.getSpec(version); + var type = getParameterType(param); + var isModel = false; + + if (type === 'object' || isModelType(spec, type)) { + isModel = true; + } else if (type === 'array' && isModelType(spec, param.items ? + param.items.type || param.items.$ref : + undefined)) { + isModel = true; + } - return isModel; + return isModel; }; -module.exports.getParameterValue = function (version, parameter, pathKeys, match, req, debug) { - var defaultVal = version === '1.2' ? parameter.defaultValue : parameter.default; - var paramLocation = version === '1.2' ? parameter.paramType : parameter.in; - var paramType = getParameterType(parameter); - var val; - - // Get the value to validate based on the operation parameter type - switch (paramLocation) { - case 'body': - val = req.body; - - break; - case 'form': - case 'formData': - if (paramType.toLowerCase() === 'file') { - if (_.isArray(req.files)) { - val = _.find(req.files, function (file) { - return file.fieldname === parameter.name; - }); - } else if (!_.isUndefined(req.files)) { - val = req.files[parameter.name] ? req.files[parameter.name] : undefined; - } - - // Swagger does not allow an array of files - if (_.isArray(val)) { - val = val[0]; - } - } else if (isModelParameter(version, parameter)) { - val = req.body; - } else { - val = req.body[parameter.name]; +module.exports.getParameterValue = function(version, parameter, pathKeys, match, req, debug) { + var defaultVal = version === '1.2' ? parameter.defaultValue : parameter.default; + var paramLocation = version === '1.2' ? parameter.paramType : parameter.in; + var paramType = getParameterType(parameter); + var val; + + // Get the value to validate based on the operation parameter type + switch (paramLocation) { + case 'body': + val = req.body; + + break; + case 'form': + case 'formData': + if (paramType.toLowerCase() === 'file') { + if (_.isArray(req.files)) { + val = _.find(req.files, function(file) { + return file.fieldname === parameter.name; + }); + } else if (!_.isUndefined(req.files)) { + val = req.files[parameter.name] ? req.files[parameter.name] : undefined; + } + + // Swagger does not allow an array of files + if (_.isArray(val)) { + val = val[0]; + } + } else if (paramType.toLowerCase() === 'array' && version === '2.0' && parameter.items.format == 'binary') { + val = req.files ? req.files[parameter.name] : undefined; + } else if (isModelParameter(version, parameter)) { + val = req.body; + } else { + val = req.body[parameter.name]; + } + + break; + case 'header': + val = req.headers[parameter.name.toLowerCase()]; + + break; + case 'path': + _.each(pathKeys, function(key, index) { + if (key.name === parameter.name) { + val = decodeURIComponent(match[index + 1]); + } + }); + + break; + case 'query': + val = _.get(req.query, parameter.name); + + break; } - break; - case 'header': - val = req.headers[parameter.name.toLowerCase()]; + debug(' Value provided: %s', !_.isUndefined(val)); - break; - case 'path': - _.each(pathKeys, function (key, index) { - if (key.name === parameter.name) { - val = decodeURIComponent(match[index + 1]); - } - }); - - break; - case 'query': - val = _.get(req.query, parameter.name); - - break; - } - - debug(' Value provided: %s', !_.isUndefined(val)); - - // Use the default value when necessary - if (_.isUndefined(val) && !_.isUndefined(defaultVal)) { - val = defaultVal; - } + // Use the default value when necessary + if (_.isUndefined(val) && !_.isUndefined(defaultVal)) { + val = defaultVal; + } - return val; + return val; }; module.exports.parseQueryString = function(req) { - return req.url.indexOf('?') > -1 ? qs.parse(parseurl(req).query, {}) : {}; + return req.url.indexOf('?') > -1 ? qs.parse(parseurl(req).query, {}) : {}; }; -module.exports.debugError = function (err, debug) { - var reason = err.message.replace(/^.*validation failed: /, ''); +module.exports.debugError = function(err, debug) { + var reason = err.message.replace(/^.*validation failed: /, ''); - reason = reason.charAt(0).toUpperCase() + reason.substring(1); + reason = reason.charAt(0).toUpperCase() + reason.substring(1); - debug(' Reason: %s', reason); + debug(' Reason: %s', reason); - if (err.failedValidation === true) { - if (err.results) { - debug(' Errors:'); + if (err.failedValidation === true) { + if (err.results) { + debug(' Errors:'); - _.each(err.results.errors, function (error, index) { - debug(' %d:', index); - debug(' code: %s', error.code); - debug(' message: %s', error.message); - debug(' path: %s', JSON.stringify(error.path)); - }); - } - } - - if (err.stack) { - debug(' Stack:'); - - _.each(err.stack.split('\n'), function (line, index) { - // Skip the first line since it's in the reasonx - if (index > 0) { - debug(' %s', line); - } - }); - } -}; - -var convertValue = module.exports.convertValue = function (value, schema, type, location) { - var original = value; - - // Default to {} - if (_.isUndefined(schema)) { - schema = {}; - } - - // Try to find the type or default to 'object' - if (_.isUndefined(type)) { - type = getParameterType(schema); - } - - // If there is no value, do not convert it - if (_.isUndefined(value)) { - return value; - } - - // If there is an empty value and allowEmptyValue is true, return it - if (schema.allowEmptyValue && value === '') { - return value; - } - - switch (type) { - case 'array': - if (_.isString(value)) { - switch (schema.collectionFormat) { - case 'csv': - case undefined: - try { - value = JSON.parse(value); - } catch (err) { - value = original; + _.each(err.results.errors, function(error, index) { + debug(' %d:', index); + debug(' code: %s', error.code); + debug(' message: %s', error.message); + debug(' path: %s', JSON.stringify(error.path)); + }); } - - if (_.isString(value)) { - value = value.split(','); - } - break; - case 'multi': - value = [value]; - break; - case 'pipes': - value = value.split('|'); - break; - case 'ssv': - value = value.split(' '); - break; - case 'tsv': - value = value.split('\t'); - break; - } } - // Handle situation where the expected type is array but only one value was provided - if (!_.isArray(value)) { - // Do not convert non-Array items to single item arrays if the location is 'body' (Issue #438) - if (location !== 'body') { - value = [value]; - } - } + if (err.stack) { + debug(' Stack:'); - if (_.isArray(value)) { - value = _.map(value, function (item, index) { - var iSchema = _.isArray(schema.items) ? schema.items[index] : schema.items; - - return convertValue(item, iSchema, iSchema ? iSchema.type : undefined, location); - }); - } - - break; - - case 'boolean': - if (!_.isBoolean(value)) { - if (['false', 'true'].indexOf(value) === -1) { - value = original; - } else { - value = value === 'true' || value === true ? true : false; - } + _.each(err.stack.split('\n'), function(line, index) { + // Skip the first line since it's in the reasonx + if (index > 0) { + debug(' %s', line); + } + }); } +}; - break; - - case 'integer': - if (!_.isNumber(value)) { - if (_.isString(value) && _.trim(value).length === 0) { - value = NaN; - } - - value = Number(value); +var convertValue = module.exports.convertValue = function(value, schema, type, location) { + var original = value; - if (isNaN(value)) { - value = original; - } + // Default to {} + if (_.isUndefined(schema)) { + schema = {}; } - break; - - case 'number': - if (!_.isNumber(value)) { - if (_.isString(value) && _.trim(value).length === 0) { - value = NaN; - } - - value = Number(value); - - if (isNaN(value)) { - value = original; - } + // Try to find the type or default to 'object' + if (_.isUndefined(type)) { + type = getParameterType(schema); } - break; - - case 'object': - if (_.isString(value)) { - try { - value = JSON.parse(value); - } catch (err) { - value = original; - } + // If there is no value, do not convert it + if (_.isUndefined(value)) { + return value; } - break; - - case 'string': - if(!_.isDate(value)) { - var isDate = schema.format === 'date' && validators.isValidDate(value); - var isDateTime = schema.format === 'date-time' && validators.isValidDateTime(value); - if (isDate || isDateTime) { - value = new Date(value); - - if (!_.isDate(value) || value.toString() === 'Invalid Date') { - value = original; - } - } + // If there is an empty value and allowEmptyValue is true, return it + if (schema.allowEmptyValue && value === '') { + return value; } - break; + switch (type) { + case 'array': + if (_.isString(value)) { + switch (schema.collectionFormat) { + case 'csv': + case undefined: + try { + value = JSON.parse(value); + } catch (err) { + value = original; + } + + if (_.isString(value)) { + value = value.split(','); + } + break; + case 'multi': + value = [value]; + break; + case 'pipes': + value = value.split('|'); + break; + case 'ssv': + value = value.split(' '); + break; + case 'tsv': + value = value.split('\t'); + break; + } + } + + // Handle situation where the expected type is array but only one value was provided + if (!_.isArray(value)) { + // Do not convert non-Array items to single item arrays if the location is 'body' (Issue #438) + if (location !== 'body') { + value = [value]; + } + } + + if (_.isArray(value)) { + value = _.map(value, function(item, index) { + var iSchema = _.isArray(schema.items) ? schema.items[index] : schema.items; + + return convertValue(item, iSchema, iSchema ? iSchema.type : undefined, location); + }); + } + + break; + + case 'boolean': + if (!_.isBoolean(value)) { + if (['false', 'true'].indexOf(value) === -1) { + value = original; + } else { + value = value === 'true' || value === true ? true : false; + } + } + + break; + + case 'integer': + if (!_.isNumber(value)) { + if (_.isString(value) && _.trim(value).length === 0) { + value = NaN; + } + + value = Number(value); + + if (isNaN(value)) { + value = original; + } + } + + break; + + case 'number': + if (!_.isNumber(value)) { + if (_.isString(value) && _.trim(value).length === 0) { + value = NaN; + } + + value = Number(value); + + if (isNaN(value)) { + value = original; + } + } + + break; + + case 'object': + if (_.isString(value)) { + try { + value = JSON.parse(value); + } catch (err) { + value = original; + } + } + + break; + + case 'string': + if (!_.isDate(value)) { + var isDate = schema.format === 'date' && validators.isValidDate(value); + var isDateTime = schema.format === 'date-time' && validators.isValidDateTime(value); + if (isDate || isDateTime) { + value = new Date(value); + + if (!_.isDate(value) || value.toString() === 'Invalid Date') { + value = original; + } + } + } + + break; - } + } - return value; + return value; }; diff --git a/middleware/swagger-metadata.js b/middleware/swagger-metadata.js index 1a7521aab4..d17f7d1146 100644 --- a/middleware/swagger-metadata.js +++ b/middleware/swagger-metadata.js @@ -36,326 +36,334 @@ var pathToRegexp = require('path-to-regexp'); // Upstream middlewares var bodyParserOptions = { - extended: false + extended: false }; var multerOptions = { - storage: multer.memoryStorage() + storage: multer.memoryStorage() }; var textBodyParserOptions = { - type: '*/*' + type: '*/*' }; var jsonBodyParser = bp.json(); var parseQueryString = mHelpers.parseQueryString; -var queryParser = function (req, res, next) { - if (_.isUndefined(req.query)) { - req.query = parseQueryString(req); - } +var queryParser = function(req, res, next) { + if (_.isUndefined(req.query)) { + req.query = parseQueryString(req); + } - return next(); + return next(); }; var realTextBodyParser = bp.text(textBodyParserOptions); -var textBodyParser = function (req, res, next) { - if (_.isUndefined(req.body)) { - realTextBodyParser(req, res, next); - } else { - next(); - } +var textBodyParser = function(req, res, next) { + if (_.isUndefined(req.body)) { + realTextBodyParser(req, res, next); + } else { + next(); + } }; var urlEncodedBodyParser = bp.urlencoded(bodyParserOptions); -var bodyParser = function (req, res, next) { - if (_.isUndefined(req.body)) { - urlEncodedBodyParser(req, res, function (err) { - if (err) { - next(err); - } else { - jsonBodyParser(req, res, next); - } - }); - } else { - next(); - } -}; -var realMultiPartParser = multer(multerOptions); -var makeMultiPartParser = function (parser) { - return function (req, res, next) { - if (_.isUndefined(req.files)) { - parser(req, res, next); +var bodyParser = function(req, res, next) { + if (_.isUndefined(req.body)) { + urlEncodedBodyParser(req, res, function(err) { + if (err) { + next(err); + } else { + jsonBodyParser(req, res, next); + } + }); } else { - next(); + next(); } - }; +}; +var realMultiPartParser = multer(multerOptions); +var makeMultiPartParser = function(parser) { + return function(req, res, next) { + if (_.isUndefined(req.files)) { + parser(req, res, next); + } else { + next(); + } + }; }; // Helper functions -var expressStylePath = function (basePath, apiPath) { - basePath = parseurl({url: basePath || '/'}).pathname || '/'; - - // Make sure the base path starts with '/' - if (basePath.charAt(0) !== '/') { - basePath = '/' + basePath; - } - - // Make sure the base path ends with '/' - if (basePath.charAt(basePath.length - 1) !== '/') { - basePath = basePath + '/'; - } - - // Make sure the api path does not start with '/' since the base path will end with '/' - if (apiPath.charAt(0) === '/') { - apiPath = apiPath.substring(1); - } - - // Replace Swagger syntax for path parameters with Express' version (All Swagger path parameters are required) - return (basePath + apiPath).replace(/{/g, ':').replace(/}/g, ''); -}; +var expressStylePath = function(basePath, apiPath) { + basePath = parseurl({ url: basePath || '/' }).pathname || '/'; -var processOperationParameters = function (swaggerMetadata, pathKeys, pathMatch, req, res, next) { - var version = swaggerMetadata.swaggerVersion; - var spec = cHelpers.getSpec(cHelpers.getSwaggerVersion(version === '1.2' ? - swaggerMetadata.resourceListing : - swaggerMetadata.swaggerObject), true); - var parameters = !_.isUndefined(swaggerMetadata) ? - (version === '1.2' ? swaggerMetadata.operation.parameters : swaggerMetadata.operationParameters) : - undefined; + // Make sure the base path starts with '/' + if (basePath.charAt(0) !== '/') { + basePath = '/' + basePath; + } - if (!parameters) { - return next(); - } + // Make sure the base path ends with '/' + if (basePath.charAt(basePath.length - 1) !== '/') { + basePath = basePath + '/'; + } + + // Make sure the api path does not start with '/' since the base path will end with '/' + if (apiPath.charAt(0) === '/') { + apiPath = apiPath.substring(1); + } - debug(' Processing Parameters'); + // Replace Swagger syntax for path parameters with Express' version (All Swagger path parameters are required) + return (basePath + apiPath).replace(/{/g, ':').replace(/}/g, ''); +}; - var parsers = _.reduce(parameters, function (requestParsers, parameter) { - var contentType = req.headers['content-type']; - var paramLocation = version === '1.2' ? parameter.paramType : parameter.schema.in; - var paramType = mHelpers.getParameterType(version === '1.2' ? parameter : parameter.schema); - var parsableBody = mHelpers.isModelType(spec, paramType) || ['array', 'object'].indexOf(paramType) > -1; - var parser; - - switch (paramLocation) { - case 'body': - case 'form': - case 'formData': - if (paramType.toLowerCase() === 'file' || (contentType && contentType.split(';')[0] === 'multipart/form-data')) { - // Do not add a parser, multipart will be handled after - break; - } else if (paramLocation !== 'body' || parsableBody) { - parser = bodyParser; - } else { - parser = textBodyParser; +var processOperationParameters = function(swaggerMetadata, pathKeys, pathMatch, req, res, next) { + var version = swaggerMetadata.swaggerVersion; + var spec = cHelpers.getSpec(cHelpers.getSwaggerVersion(version === '1.2' ? + swaggerMetadata.resourceListing : + swaggerMetadata.swaggerObject), true); + var parameters = !_.isUndefined(swaggerMetadata) ? + (version === '1.2' ? swaggerMetadata.operation.parameters : swaggerMetadata.operationParameters) : + undefined; + + if (!parameters) { + return next(); + } + + debug(' Processing Parameters'); + + var parsers = _.reduce(parameters, function(requestParsers, parameter) { + var contentType = req.headers['content-type']; + var paramLocation = version === '1.2' ? parameter.paramType : parameter.schema.in; + var paramType = mHelpers.getParameterType(version === '1.2' ? parameter : parameter.schema); + var parsableBody = mHelpers.isModelType(spec, paramType) || ['array', 'object'].indexOf(paramType) > -1; + var parser; + + switch (paramLocation) { + case 'body': + case 'form': + case 'formData': + if (paramType.toLowerCase() === 'file' || (contentType && contentType.split(';')[0] === 'multipart/form-data')) { + // Do not add a parser, multipart will be handled after + break; + } else if (paramLocation !== 'body' || parsableBody) { + parser = bodyParser; + } else { + parser = textBodyParser; + } + + break; + + case 'query': + parser = queryParser; + + break; } - break; + if (parser && requestParsers.indexOf(parser) === -1) { + requestParsers.push(parser); + } - case 'query': - parser = queryParser; + return requestParsers; + }, []); + + // Multipart is handled by multer, which needs an array of {parameterName, maxCount} + var multiPartFields = _.reduce(parameters, function(fields, parameter) { + var paramLocation = version === '1.2' ? parameter.paramType : parameter.schema.in; + var paramType = mHelpers.getParameterType(version === '1.2' ? parameter : parameter.schema); + var paramName = version === '1.2' ? parameter.name : parameter.schema.name; + + switch (paramLocation) { + case 'body': + case 'form': + case 'formData': + switch (paramType.toLowerCase()) { + case 'file': + // Swagger spec does not allow array of files, so maxCount should be 1 + fields.push({ name: paramName, maxCount: 1 }); + break; + case 'array': + if (version === '2.0' && parameter.schema.items.format == 'binary') { + // Swagger spec allows array of files in this form, so maxCount is not necessary + // https://swagger.io/docs/specification/2-0/file-upload/ + fields.push({ name: paramName }); + } + break; + default: + break; + } + break; + } - break; - } + return fields; + }, []); - if (parser && requestParsers.indexOf(parser) === -1) { - requestParsers.push(parser); + var contentType = req.headers['content-type']; + if (multiPartFields.length) { + // If there are files, use multer#fields + parsers.push(makeMultiPartParser(realMultiPartParser.fields(multiPartFields))); + } else if (contentType && contentType.split(';')[0] === 'multipart/form-data') { + // If no files but multipart form, use empty multer#array for text fields + parsers.push(makeMultiPartParser(realMultiPartParser.array())); } - return requestParsers; - }, []); - - // Multipart is handled by multer, which needs an array of {parameterName, maxCount} - var multiPartFields = _.reduce(parameters, function (fields, parameter) { - var paramLocation = version === '1.2' ? parameter.paramType : parameter.schema.in; - var paramType = mHelpers.getParameterType(version === '1.2' ? parameter : parameter.schema); - var paramName = version === '1.2' ? parameter.name : parameter.schema.name; - - switch (paramLocation) { - case 'body': - case 'form': - case 'formData': - if (paramType.toLowerCase() === 'file') { - // Swagger spec does not allow array of files, so maxCount should be 1 - fields.push({name: paramName, maxCount: 1}); + async.map(parsers, function(parser, callback) { + parser(req, res, callback); + }, function(err) { + if (err) { + return next(err); } - break; - } - return fields; - }, []); - - var contentType = req.headers['content-type']; - if (multiPartFields.length) { - // If there are files, use multer#fields - parsers.push(makeMultiPartParser(realMultiPartParser.fields(multiPartFields))); - } else if (contentType && contentType.split(';')[0] === 'multipart/form-data') { - // If no files but multipart form, use empty multer#array for text fields - parsers.push(makeMultiPartParser(realMultiPartParser.array())); - } - - async.map(parsers, function (parser, callback) { - parser(req, res, callback); - }, function (err) { - if (err) { - return next(err); - } + _.each(parameters, function(parameterOrMetadata, index) { + var parameter = version === '1.2' ? parameterOrMetadata : parameterOrMetadata.schema; + var pLocation = version === '1.2' ? parameter.paramType : parameter.in; + var pType = mHelpers.getParameterType(parameter); + var oVal; + var value; - _.each(parameters, function (parameterOrMetadata, index) { - var parameter = version === '1.2' ? parameterOrMetadata : parameterOrMetadata.schema; - var pLocation = version === '1.2' ? parameter.paramType : parameter.in; - var pType = mHelpers.getParameterType(parameter); - var oVal; - var value; - - debug(' %s', parameter.name); - debug(' Type: %s%s', pType, !_.isUndefined(parameter.format) ? ' (format: ' + parameter.format + ')': ''); - - // Located here to make the debug output pretty - oVal = mHelpers.getParameterValue(version, parameter, pathKeys, pathMatch, req, debug); - value = mHelpers.convertValue(oVal, _.isUndefined(parameter.schema) ? parameter : parameter.schema, pType, pLocation); - - debug(' Value: %s', value); - - swaggerMetadata.params[parameter.name] = { - path: version === '1.2' ? - swaggerMetadata.operationPath.concat(['parameters', index.toString()]) : - parameterOrMetadata.path, - schema: parameter, - originalValue: oVal, - value: value - }; - }); + debug(' %s', parameter.name); + debug(' Type: %s%s', pType, !_.isUndefined(parameter.format) ? ' (format: ' + parameter.format + ')' : ''); - return next(); - }); -}; -var processSwaggerDocuments = function (rlOrSO, apiDeclarations) { - if (_.isUndefined(rlOrSO)) { - throw new Error('rlOrSO is required'); - } else if (!_.isPlainObject(rlOrSO)) { - throw new TypeError('rlOrSO must be an object'); - } - - var spec = cHelpers.getSpec(cHelpers.getSwaggerVersion(rlOrSO), true); - var apiCache = {}; - var composeParameters = function (apiPath, method, path, operation) { - var cParams = []; - var seenParams = []; - - _.each(operation.parameters, function (parameter, index) { - cParams.push({ - path: apiPath.concat([method, 'parameters', index.toString()]), - schema: parameter - }); - - seenParams.push(parameter.name + ':' + parameter.in); - }); + // Located here to make the debug output pretty + oVal = mHelpers.getParameterValue(version, parameter, pathKeys, pathMatch, req, debug); + value = mHelpers.convertValue(oVal, _.isUndefined(parameter.schema) ? parameter : parameter.schema, pType, pLocation); - _.each(path.parameters, function (parameter, index) { - if (seenParams.indexOf(parameter.name + ':' + parameter.in) === -1) { - cParams.push({ - path: apiPath.concat(['parameters', index.toString()]), - schema: parameter + debug(' Value: %s', value); + + swaggerMetadata.params[parameter.name] = { + path: version === '1.2' ? + swaggerMetadata.operationPath.concat(['parameters', index.toString()]) : parameterOrMetadata.path, + schema: parameter, + originalValue: oVal, + value: value + }; }); - } - }); - return cParams; - }; - var createCacheEntry = function (adOrSO, apiOrPath, indexOrName, indent) { - var apiPath = spec.version === '1.2' ? apiOrPath.path : indexOrName; - var expressPath = expressStylePath(adOrSO.basePath, spec.version === '1.2' ? apiOrPath.path: indexOrName); - var keys = []; - var handleSubPaths = !(rlOrSO.paths && rlOrSO.paths[apiPath]['x-swagger-router-handle-subpaths']); - var re = pathToRegexp(expressPath, keys, { end: handleSubPaths }); - var cacheKey = re.toString(); - var cacheEntry; - - // This is an absolute path, use it as the cache key - if (expressPath.indexOf('{') === -1) { - cacheKey = expressPath; + return next(); + }); +}; +var processSwaggerDocuments = function(rlOrSO, apiDeclarations) { + if (_.isUndefined(rlOrSO)) { + throw new Error('rlOrSO is required'); + } else if (!_.isPlainObject(rlOrSO)) { + throw new TypeError('rlOrSO must be an object'); } - debug(new Array(indent + 1).join(' ') + 'Found %s: %s', - (spec.version === '1.2' ? 'API' : 'Path'), - apiPath); - - cacheEntry = apiCache[cacheKey] = spec.version === '1.2' ? - { - api: apiOrPath, - apiDeclaration: adOrSO, - apiIndex: indexOrName, - keys: keys, - params: {}, - re: re, - operations: {}, - resourceListing: rlOrSO - } : - { - apiPath: indexOrName, - path: apiOrPath, - keys: keys, - re: re, - operations: {}, - swaggerObject: { - original: rlOrSO, - resolved: adOrSO + var spec = cHelpers.getSpec(cHelpers.getSwaggerVersion(rlOrSO), true); + var apiCache = {}; + var composeParameters = function(apiPath, method, path, operation) { + var cParams = []; + var seenParams = []; + + _.each(operation.parameters, function(parameter, index) { + cParams.push({ + path: apiPath.concat([method, 'parameters', index.toString()]), + schema: parameter + }); + + seenParams.push(parameter.name + ':' + parameter.in); + }); + + _.each(path.parameters, function(parameter, index) { + if (seenParams.indexOf(parameter.name + ':' + parameter.in) === -1) { + cParams.push({ + path: apiPath.concat(['parameters', index.toString()]), + schema: parameter + }); + } + }); + + return cParams; + }; + var createCacheEntry = function(adOrSO, apiOrPath, indexOrName, indent) { + var apiPath = spec.version === '1.2' ? apiOrPath.path : indexOrName; + var expressPath = expressStylePath(adOrSO.basePath, spec.version === '1.2' ? apiOrPath.path : indexOrName); + var keys = []; + var handleSubPaths = !(rlOrSO.paths && rlOrSO.paths[apiPath]['x-swagger-router-handle-subpaths']); + var re = pathToRegexp(expressPath, keys, { end: handleSubPaths }); + var cacheKey = re.toString(); + var cacheEntry; + + // This is an absolute path, use it as the cache key + if (expressPath.indexOf('{') === -1) { + cacheKey = expressPath; } - }; - return cacheEntry; - }; + debug(new Array(indent + 1).join(' ') + 'Found %s: %s', + (spec.version === '1.2' ? 'API' : 'Path'), + apiPath); + + cacheEntry = apiCache[cacheKey] = spec.version === '1.2' ? { + api: apiOrPath, + apiDeclaration: adOrSO, + apiIndex: indexOrName, + keys: keys, + params: {}, + re: re, + operations: {}, + resourceListing: rlOrSO + } : { + apiPath: indexOrName, + path: apiOrPath, + keys: keys, + re: re, + operations: {}, + swaggerObject: { + original: rlOrSO, + resolved: adOrSO + } + }; + + return cacheEntry; + }; - debug(' Identified Swagger version: %s', spec.version); + debug(' Identified Swagger version: %s', spec.version); - if (spec.version === '1.2') { - if (_.isUndefined(apiDeclarations)) { - throw new Error('apiDeclarations is required'); - } else if (!_.isArray(apiDeclarations)) { - throw new TypeError('apiDeclarations must be an array'); - } + if (spec.version === '1.2') { + if (_.isUndefined(apiDeclarations)) { + throw new Error('apiDeclarations is required'); + } else if (!_.isArray(apiDeclarations)) { + throw new TypeError('apiDeclarations must be an array'); + } - debug(' Number of API Declarations: %d', apiDeclarations.length); + debug(' Number of API Declarations: %d', apiDeclarations.length); - _.each(apiDeclarations, function (apiDeclaration, adIndex) { - debug(' Processing API Declaration %d', adIndex); + _.each(apiDeclarations, function(apiDeclaration, adIndex) { + debug(' Processing API Declaration %d', adIndex); - _.each(apiDeclaration.apis, function (api, apiIndex) { - var cacheEntry = createCacheEntry(apiDeclaration, api, apiIndex, 4); + _.each(apiDeclaration.apis, function(api, apiIndex) { + var cacheEntry = createCacheEntry(apiDeclaration, api, apiIndex, 4); - cacheEntry.resourceIndex = adIndex; + cacheEntry.resourceIndex = adIndex; - _.each(api.operations, function (operation, operationIndex) { - cacheEntry.operations[operation.method.toLowerCase()] = { - operation: operation, - operationPath: ['apis', apiIndex.toString(), 'operations', operationIndex.toString()], - operationParameters: operation.parameters - }; + _.each(api.operations, function(operation, operationIndex) { + cacheEntry.operations[operation.method.toLowerCase()] = { + operation: operation, + operationPath: ['apis', apiIndex.toString(), 'operations', operationIndex.toString()], + operationParameters: operation.parameters + }; + }); + }); }); - }); - }); - } else { - // To avoid running into issues with references throughout the Swagger object we will use the resolved version. - // Getting the resolved version is an asynchronous process but since initializeMiddleware caches the resolved document - // this is a synchronous action at this point. - spec.resolve(rlOrSO, function (err, resolved) { - // Gather the paths, their path regex patterns and the corresponding operations - _.each(resolved.paths, function (path, pathName) { - var cacheEntry = createCacheEntry(resolved, path, pathName, 2); - - _.each(['get', 'put', 'post', 'delete', 'options', 'head', 'patch'], function (method) { - var operation = path[method]; - - if (!_.isUndefined(operation)) { - cacheEntry.operations[method] = { - operation: operation, - operationPath: ['paths', pathName, method], - // Required since we have to compose parameters based on the operation and the path - operationParameters: composeParameters(['paths', pathName], method, path, operation) - }; - } + } else { + // To avoid running into issues with references throughout the Swagger object we will use the resolved version. + // Getting the resolved version is an asynchronous process but since initializeMiddleware caches the resolved document + // this is a synchronous action at this point. + spec.resolve(rlOrSO, function(err, resolved) { + // Gather the paths, their path regex patterns and the corresponding operations + _.each(resolved.paths, function(path, pathName) { + var cacheEntry = createCacheEntry(resolved, path, pathName, 2); + + _.each(['get', 'put', 'post', 'delete', 'options', 'head', 'patch'], function(method) { + var operation = path[method]; + + if (!_.isUndefined(operation)) { + cacheEntry.operations[method] = { + operation: operation, + operationPath: ['paths', pathName, method], + // Required since we have to compose parameters based on the operation and the path + operationParameters: composeParameters(['paths', pathName], method, path, operation) + }; + } + }); + }); }); - }); - }); - } + } - return apiCache; + return apiCache; }; /** @@ -370,85 +378,83 @@ var processSwaggerDocuments = function (rlOrSO, apiDeclarations) { * * @returns the middleware function */ -exports = module.exports = function (rlOrSO, apiDeclarations) { - debug('Initializing swagger-metadata middleware'); - - var apiCache = processSwaggerDocuments(rlOrSO, apiDeclarations); - var swaggerVersion = cHelpers.getSwaggerVersion(rlOrSO); - - if (_.isUndefined(rlOrSO)) { - throw new Error('rlOrSO is required'); - } else if (!_.isPlainObject(rlOrSO)) { - throw new TypeError('rlOrSO must be an object'); - } - - if (swaggerVersion === '1.2') { - if (_.isUndefined(apiDeclarations)) { - throw new Error('apiDeclarations is required'); - } else if (!_.isArray(apiDeclarations)) { - throw new TypeError('apiDeclarations must be an array'); - } - } - - return function swaggerMetadata (req, res, next) { - var method = req.method.toLowerCase(); - var path = parseurl(req).pathname; - var cacheEntry; - var match; - var metadata; - - cacheEntry = apiCache[path] || _.find(apiCache, function (metadata) { - match = metadata.re.exec(path); - return _.isArray(match); - }); +exports = module.exports = function(rlOrSO, apiDeclarations) { + debug('Initializing swagger-metadata middleware'); - debug('%s %s', req.method, req.url); - debug(' Is a Swagger path: %s', !_.isUndefined(cacheEntry)); + var apiCache = processSwaggerDocuments(rlOrSO, apiDeclarations); + var swaggerVersion = cHelpers.getSwaggerVersion(rlOrSO); - // Request does not match an API defined in the Swagger document(s) - if (!cacheEntry) { - return next(); + if (_.isUndefined(rlOrSO)) { + throw new Error('rlOrSO is required'); + } else if (!_.isPlainObject(rlOrSO)) { + throw new TypeError('rlOrSO must be an object'); } - metadata = swaggerVersion === '1.2' ? - { - api: cacheEntry.api, - apiDeclaration: cacheEntry.apiDeclaration, - apiIndex: cacheEntry.apiIndex, - params: {}, - resourceIndex: cacheEntry.resourceIndex, - resourceListing: cacheEntry.resourceListing - } : - { - apiPath : cacheEntry.apiPath, - path: cacheEntry.path, - params: {}, - swaggerObject: cacheEntry.swaggerObject.resolved - }; + if (swaggerVersion === '1.2') { + if (_.isUndefined(apiDeclarations)) { + throw new Error('apiDeclarations is required'); + } else if (!_.isArray(apiDeclarations)) { + throw new TypeError('apiDeclarations must be an array'); + } + } - if (_.isPlainObject(cacheEntry.operations[method])) { - metadata.operation = cacheEntry.operations[method].operation; - metadata.operationPath = cacheEntry.operations[method].operationPath; + return function swaggerMetadata(req, res, next) { + var method = req.method.toLowerCase(); + var path = parseurl(req).pathname; + var cacheEntry; + var match; + var metadata; - if (swaggerVersion === '1.2') { - metadata.authorizations = metadata.operation.authorizations || cacheEntry.apiDeclaration.authorizations; - } else { - metadata.operationParameters = cacheEntry.operations[method].operationParameters; - metadata.security = metadata.operation.security || metadata.swaggerObject.security || []; - } - } + cacheEntry = apiCache[path] || _.find(apiCache, function(metadata) { + match = metadata.re.exec(path); + return _.isArray(match); + }); + + debug('%s %s', req.method, req.url); + debug(' Is a Swagger path: %s', !_.isUndefined(cacheEntry)); + + // Request does not match an API defined in the Swagger document(s) + if (!cacheEntry) { + return next(); + } - metadata.swaggerVersion = swaggerVersion; + metadata = swaggerVersion === '1.2' ? { + api: cacheEntry.api, + apiDeclaration: cacheEntry.apiDeclaration, + apiIndex: cacheEntry.apiIndex, + params: {}, + resourceIndex: cacheEntry.resourceIndex, + resourceListing: cacheEntry.resourceListing + } : { + apiPath: cacheEntry.apiPath, + path: cacheEntry.path, + params: {}, + swaggerObject: cacheEntry.swaggerObject.resolved + }; + + if (_.isPlainObject(cacheEntry.operations[method])) { + metadata.operation = cacheEntry.operations[method].operation; + metadata.operationPath = cacheEntry.operations[method].operationPath; + + if (swaggerVersion === '1.2') { + metadata.authorizations = metadata.operation.authorizations || cacheEntry.apiDeclaration.authorizations; + } else { + metadata.operationParameters = cacheEntry.operations[method].operationParameters; + metadata.security = metadata.operation.security || metadata.swaggerObject.security || []; + } + } - req.swagger = metadata; + metadata.swaggerVersion = swaggerVersion; - debug(' Is a Swagger operation: %s', !_.isUndefined(metadata.operation)); + req.swagger = metadata; - if (metadata.operation) { - // Process the operation parameters - return processOperationParameters(metadata, cacheEntry.keys, match, req, res, next, debug); - } else { - return next(); - } - }; -}; + debug(' Is a Swagger operation: %s', !_.isUndefined(metadata.operation)); + + if (metadata.operation) { + // Process the operation parameters + return processOperationParameters(metadata, cacheEntry.keys, match, req, res, next, debug); + } else { + return next(); + } + }; +}; \ No newline at end of file diff --git a/package.json b/package.json index 93d226fe85..e3c68484fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "swagger-tools", - "version": "0.10.3", + "version": "0.10.4", "description": "Various tools for using and integrating with Swagger.", "main": "index.js", "scripts": {