UNPKG

swagger-tools

Version:

Various tools for using and integrating with Swagger.

1,458 lines (1,195 loc) 52.1 kB
/* * 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. */ 'use strict'; var _ = require('lodash'); var async = require('async'); var helpers = require('./helpers'); var JsonRefs = require('json-refs'); var SparkMD5 = require('spark-md5'); var swaggerConverter = require('swagger-converter'); var traverse = require('traverse'); var validators = require('./validators'); var YAML = require('js-yaml'); // Work around swagger-converter packaging issue (Browser builds only) if (_.isPlainObject(swaggerConverter)) { swaggerConverter = global.SwaggerConverter.convert; } var documentCache = {}; var sanitizeRef = function (version, ref) { return version !== '1.2' ? ref : ref.replace('#/models/', ''); }; var swagger1RefPreProcesor = function (obj) { var pObj = _.cloneDeep(obj); pObj.$ref = '#/models/' + obj.$ref; return pObj; }; var validOptionNames = _.map(helpers.swaggerOperationMethods, function (method) { return method.toLowerCase(); }); var isRemotePtr = function (refDetails) { return ['relative', 'remote'].indexOf(refDetails.type) > -1; }; var createErrorOrWarning = function (code, message, path, dest) { dest.push({ code: code, message: message, path: path }); }; var addReference = function (cacheEntry, defPathOrPtr, refPathOrPtr, results, omitError) { var result = true; var swaggerVersion = helpers.getSwaggerVersion(cacheEntry.resolved); var defPath = _.isArray(defPathOrPtr) ? defPathOrPtr : JsonRefs.pathFromPtr(defPathOrPtr); var defPtr = _.isArray(defPathOrPtr) ? JsonRefs.pathToPtr(defPathOrPtr) : defPathOrPtr; var refPath = _.isArray(refPathOrPtr) ? refPathOrPtr : JsonRefs.pathFromPtr(refPathOrPtr); var refPtr = _.isArray(refPathOrPtr) ? JsonRefs.pathToPtr(refPathOrPtr) : refPathOrPtr; var code; var def; var displayId; var i; var msgPrefix; var type; def = cacheEntry.definitions[defPtr]; type = defPath[0]; code = type === 'securityDefinitions' ? 'SECURITY_DEFINITION' : type.substring(0, type.length - 1).toUpperCase(); displayId = swaggerVersion === '1.2' ? defPath[defPath.length - 1] : defPtr; msgPrefix = type === 'securityDefinitions' ? 'Security definition' : code.charAt(0) + code.substring(1).toLowerCase(); // This is an authorization scope reference if (['authorizations', 'securityDefinitions'].indexOf(defPath[0]) > -1 && defPath[2] === 'scopes') { code += '_SCOPE'; msgPrefix += ' scope'; } // If the reference was not found and this is not an authorization/security scope reference, attempt to find a // parent object to add the reference too. (Issue 176) if (_.isUndefined(def) && ['AUTHORIZATION_SCOPE', 'SECURITY_DEFINITION_SCOPE'].indexOf(code) === -1) { // Attempt to find the definition in case the reference is to a path within a definition` for (i = 1; i < defPath.length; i++) { var pPath = defPath.slice(0, defPath.length - i); var pPtr = JsonRefs.pathToPtr(pPath); var pDef = cacheEntry.definitions[pPtr]; if (!_.isUndefined(pDef)) { def = pDef; break; } } } if (_.isUndefined(def)) { if (!omitError) { if (cacheEntry.swaggerVersion !== '1.2' && ['SECURITY_DEFINITION', 'SECURITY_DEFINITION_SCOPE'].indexOf(code) === -1) { refPath.push('$ref'); } createErrorOrWarning('UNRESOLVABLE_' + code, msgPrefix + ' could not be resolved: ' + displayId, refPath, results.errors); } result = false; } else { if (_.isUndefined(def.references)) { def.references = []; } def.references.push(refPtr); } return result; }; var getOrComposeSchema = function (documentMetadata, modelId) { var title = 'Composed ' + (documentMetadata.swaggerVersion === '1.2' ? JsonRefs.pathFromPtr(modelId).pop() : modelId); var metadata = documentMetadata.definitions[modelId]; var originalT = traverse(documentMetadata.original); var resolvedT = traverse(documentMetadata.resolved); var composed; var original; if (!metadata) { return undefined; } original = _.cloneDeep(originalT.get(JsonRefs.pathFromPtr(modelId))); composed = _.cloneDeep(resolvedT.get(JsonRefs.pathFromPtr(modelId))); // Convert the Swagger 1.2 document to a valid JSON Schema file if (documentMetadata.swaggerVersion === '1.2') { // Create inheritance model if (metadata.lineage.length > 0) { composed.allOf = []; _.each(metadata.lineage, function (modelId) { composed.allOf.push(getOrComposeSchema(documentMetadata, modelId)); }); } // Remove the subTypes property delete composed.subTypes; _.each(composed.properties, function (property, name) { var oProp = original.properties[name]; // Convert the string values to numerical values _.each(['maximum', 'minimum'], function (prop) { if (_.isString(property[prop])) { property[prop] = parseFloat(property[prop]); } }); _.each(JsonRefs.findRefs(oProp, { includeInvalid: true, refPreProcessor: swagger1RefPreProcesor }), function (refDetails, refPtr) { var dMetadata = documentMetadata.definitions[refDetails.uri]; var path = JsonRefs.pathFromPtr(refPtr); if (dMetadata.lineage.length > 0) { traverse(property).set(path, getOrComposeSchema(documentMetadata, refDetails.uri)); } else { traverse(property).set(path.concat('title'), 'Composed ' + sanitizeRef(documentMetadata.swaggerVersion, refDetails.uri)); } }); }); } // Scrub id properties composed = traverse(composed).map(function (val) { if (this.key === 'id' && _.isString(val)) { this.remove(); } }); composed.title = title; return composed; }; var createUnusedErrorOrWarning = function (val, codeSuffix, msgPrefix, path, dest) { createErrorOrWarning('UNUSED_' + codeSuffix, msgPrefix + ' is defined but is not used: ' + val, path, dest); }; var getDocumentCache = function (apiDOrSO) { var key = SparkMD5.hash(JSON.stringify(apiDOrSO)); var cacheEntry = documentCache[key] || _.find(documentCache, function (cacheEntry) { return cacheEntry.resolvedId === key; }); if (!cacheEntry) { cacheEntry = documentCache[key] = { definitions: {}, original: apiDOrSO, resolved: undefined, swaggerVersion: helpers.getSwaggerVersion(apiDOrSO) }; } return cacheEntry; }; var handleValidationError = function (results, callback) { var err = new Error('The Swagger document(s) are invalid'); err.errors = results.errors; err.failedValidation = true; err.warnings = results.warnings; if (results.apiDeclarations) { err.apiDeclarations = results.apiDeclarations; } callback(err); }; var normalizePath = function (path) { var matches = path.match(/\{(.*?)\}/g); var argNames = []; var normPath = path; if (matches) { _.each(matches, function (match, index) { normPath = normPath.replace(match, '{' + index + '}'); argNames.push(match.replace(/[{}]/g, '')); }); } return { path: normPath, args: argNames }; }; var removeCirculars = function (obj) { function walk (ancestors, node, path) { function walkItem (item, segment) { path.push(segment); walk(ancestors, item, path); path.pop(); } // We do not process circular objects again if (ancestors.indexOf(node) === -1) { ancestors.push(node); if (_.isArray(node)) { _.each(node, function (member, index) { walkItem(member, index.toString()); }); } else if (_.isPlainObject(node)) { _.forOwn(node, function (member, key) { walkItem(member, key.toString()); }); } } else { _.set(obj, path, {}); } ancestors.pop(); } walk([], obj, []); }; var validateNoExist = function (data, val, codeSuffix, msgPrefix, path, dest) { if (!_.isUndefined(data) && data.indexOf(val) > -1) { createErrorOrWarning('DUPLICATE_' + codeSuffix, msgPrefix + ' already defined: ' + val, path, dest); } }; var validateSchemaConstraints = function (documentMetadata, schema, path, results, skip) { try { validators.validateSchemaConstraints(documentMetadata.swaggerVersion, schema, path, undefined); } catch (err) { if (!skip) { createErrorOrWarning(err.code, err.message, err.path, results.errors); } } }; var processDocument = function (documentMetadata, results) { var swaggerVersion = documentMetadata.swaggerVersion; var getDefinitionMetadata = function (defPath, inline) { var defPtr = JsonRefs.pathToPtr(defPath); var metadata = documentMetadata.definitions[defPtr]; if (!metadata) { metadata = documentMetadata.definitions[defPtr] = { inline: inline || false, references: [] }; // For model definitions, add the inheritance properties if (['definitions', 'models'].indexOf(JsonRefs.pathFromPtr(defPtr)[0]) > -1) { metadata.cyclical = false; metadata.lineage = undefined; metadata.parents = []; } } return metadata; }; var getDisplayId = function (id) { return swaggerVersion === '1.2' ? JsonRefs.pathFromPtr(id).pop() : id; }; var jsonRefsOptions = { filter: 'local', includeInvalid: true }; var walk = function (root, id, lineage) { var definition = documentMetadata.definitions[id || root]; if (definition) { _.each(definition.parents, function (parent) { lineage.push(parent); if (root !== parent) { walk(root, parent, lineage); } }); } }; var authDefsProp = swaggerVersion === '1.2' ? 'authorizations' : 'securityDefinitions'; var modelDefsProp = swaggerVersion === '1.2' ? 'models' : 'definitions'; // Process authorization definitions _.each(documentMetadata.resolved[authDefsProp], function (authorization, name) { var securityDefPath = [authDefsProp, name]; // Swagger 1.2 only has authorization definitions in the Resource Listing if (swaggerVersion === '1.2' && !authorization.type) { return; } // Create the authorization definition metadata getDefinitionMetadata(securityDefPath); _.reduce(authorization.scopes, function (seenScopes, scope, indexOrName) { var scopeName = swaggerVersion === '1.2' ? scope.scope : indexOrName; var scopeDefPath = securityDefPath.concat(['scopes', indexOrName.toString()]); var scopeMetadata = getDefinitionMetadata(securityDefPath.concat(['scopes', scopeName])); scopeMetadata.scopePath = scopeDefPath; // Identify duplicate authorization scope defined in the Resource Listing validateNoExist(seenScopes, scopeName, 'AUTHORIZATION_SCOPE_DEFINITION', 'Authorization scope definition', swaggerVersion === '1.2' ? scopeDefPath.concat('scope') : scopeDefPath, results.warnings); seenScopes.push(scopeName); return seenScopes; }, []); }); // Process model definitions _.each(documentMetadata.resolved[modelDefsProp], function (model, modelId) { var modelDefPath = [modelDefsProp, modelId]; var modelMetadata = getDefinitionMetadata(modelDefPath); // Identify model id mismatch (Id in models object is not the same as the model's id in the models object) if (swaggerVersion === '1.2' && modelId !== model.id) { createErrorOrWarning('MODEL_ID_MISMATCH', 'Model id does not match id in models object: ' + model.id, modelDefPath.concat('id'), results.errors); } // Do not reprocess parents/references if already processed if (_.isUndefined(modelMetadata.lineage)) { // Handle inheritance references switch (swaggerVersion) { case '1.2': _.each(model.subTypes, function (subType, index) { var subPath = ['models', subType]; var subPtr = JsonRefs.pathToPtr(subPath); var subMetadata = documentMetadata.definitions[subPtr]; var refPath = modelDefPath.concat(['subTypes', index.toString()]); // If the metadata does not yet exist, create it if (!subMetadata && documentMetadata.resolved[modelDefsProp][subType]) { subMetadata = getDefinitionMetadata(subPath); } // If the reference is valid, add the parent if (addReference(documentMetadata, subPath, refPath, results)) { subMetadata.parents.push(JsonRefs.pathToPtr(modelDefPath)); } }); break; default: _.each(documentMetadata.original[modelDefsProp][modelId].allOf, function (schema, index) { var isInline = false; var parentPath; if (_.isUndefined(schema.$ref) || isRemotePtr(JsonRefs.getRefDetails(schema))) { isInline = true; parentPath = modelDefPath.concat(['allOf', index.toString()]); } else { parentPath = JsonRefs.pathFromPtr(schema.$ref); } // If the parent model does not exist, do not create its metadata if (!_.isUndefined(traverse(documentMetadata.resolved).get(parentPath))) { // Create metadata for parent getDefinitionMetadata(parentPath, isInline); modelMetadata.parents.push(JsonRefs.pathToPtr(parentPath)); } }); break; } } }); switch (swaggerVersion) { case '2.0': // Process parameter definitions _.each(documentMetadata.resolved.parameters, function (parameter, name) { var path = ['parameters', name]; getDefinitionMetadata(path); validateSchemaConstraints(documentMetadata, parameter, path, results); }); // Process response definitions _.each(documentMetadata.resolved.responses, function (response, name) { var path = ['responses', name]; getDefinitionMetadata(path); validateSchemaConstraints(documentMetadata, response, path, results); }); break; } // Validate definition/models (Inheritance, property definitions, ...) _.each(documentMetadata.definitions, function (metadata, id) { var defPath = JsonRefs.pathFromPtr(id); var definition = traverse(documentMetadata.original).get(defPath); var defProp = defPath[0]; var code = defProp.substring(0, defProp.length - 1).toUpperCase(); var msgPrefix = code.charAt(0) + code.substring(1).toLowerCase(); var dProperties; var iProperties; var lineage; // The only checks we perform below are inheritance checks so skip all non-model definitions if (['definitions', 'models'].indexOf(defProp) === -1) { return; } dProperties = []; iProperties = []; lineage = metadata.lineage; // Do not reprocess lineage if already processed if (_.isUndefined(lineage)) { lineage = []; walk(id, undefined, lineage); // Root > next > ... lineage.reverse(); metadata.lineage = _.cloneDeep(lineage); metadata.cyclical = lineage.length > 1 && lineage[0] === id; } // Swagger 1.2 does not allow multiple inheritance while Swagger 2.0+ does if (metadata.parents.length > 1 && swaggerVersion === '1.2') { createErrorOrWarning('MULTIPLE_' + code + '_INHERITANCE', 'Child ' + code.toLowerCase() + ' is sub type of multiple models: ' + _.map(metadata.parents, function (parent) { return getDisplayId(parent); }).join(' && '), defPath, results.errors); } if (metadata.cyclical) { createErrorOrWarning('CYCLICAL_' + code + '_INHERITANCE', msgPrefix + ' has a circular inheritance: ' + _.map(lineage, function (dep) { return getDisplayId(dep); }).join(' -> ') + ' -> ' + getDisplayId(id), defPath.concat(swaggerVersion === '1.2' ? 'subTypes' : 'allOf'), results.errors); } // Remove self reference from the end of the lineage (Front too if cyclical) _.each(lineage.slice(metadata.cyclical ? 1 : 0), function (id) { var pModel = traverse(documentMetadata.resolved).get(JsonRefs.pathFromPtr(id)); _.each(Object.keys(pModel.properties || {}), function (name) { if (iProperties.indexOf(name) === -1) { iProperties.push(name); } }); }); // Validate simple definitions validateSchemaConstraints(documentMetadata, definition, defPath, results); // Identify redeclared properties _.each(definition.properties, function (property, name) { var pPath = defPath.concat(['properties', name]); // Do not process unresolved properties if (!_.isUndefined(property)) { validateSchemaConstraints(documentMetadata, property, pPath, results); if (iProperties.indexOf(name) > -1) { createErrorOrWarning('CHILD_' + code + '_REDECLARES_PROPERTY', 'Child ' + code.toLowerCase() + ' declares property already declared by ancestor: ' + name, pPath, results.errors); } else { dProperties.push(name); } } }); // Identify missing required properties _.each(definition.required || [], function (name, index) { var type = swaggerVersion === '1.2' ? 'Model' : 'Definition'; if (iProperties.indexOf(name) === -1 && dProperties.indexOf(name) === -1) { createErrorOrWarning('MISSING_REQUIRED_' + type.toUpperCase() + '_PROPERTY', type + ' requires property but it is not defined: ' + name, defPath.concat(['required', index.toString()]), results.errors); } }); }); if (documentMetadata.swaggerVersion === '1.2') { jsonRefsOptions.refPreProcessor = swagger1RefPreProcesor; } // Process local references _.each(JsonRefs.findRefs(documentMetadata.original, jsonRefsOptions), function (refDetails, refPtr) { addReference(documentMetadata, refDetails.uri, refPtr, results); }); // Process invalid references _.each(documentMetadata.referencesMetadata, function (refDetails, refPtr) { if (isRemotePtr(refDetails) && refDetails.missing === true) { results.errors.push({ code: 'UNRESOLVABLE_REFERENCE', message: 'Reference could not be resolved: ' + sanitizeRef(documentMetadata.swaggerVersion, refDetails.uri), path: JsonRefs.pathFromPtr(refPtr).concat('$ref') }); } }); }; var validateExist = function (data, val, codeSuffix, msgPrefix, path, dest) { if (!_.isUndefined(data) && data.indexOf(val) === -1) { createErrorOrWarning('UNRESOLVABLE_' + codeSuffix, msgPrefix + ' could not be resolved: ' + val, path, dest); } }; var processAuthRefs = function (documentMetadata, authRefs, path, results) { var code = documentMetadata.swaggerVersion === '1.2' ? 'AUTHORIZATION' : 'SECURITY_DEFINITION'; var msgPrefix = code === 'AUTHORIZATION' ? 'Authorization' : 'Security definition'; if (documentMetadata.swaggerVersion === '1.2') { _.reduce(authRefs, function (seenNames, scopes, name) { var authPtr = ['authorizations', name]; var aPath = path.concat([name]); // Add reference or record unresolved authorization if (addReference(documentMetadata, authPtr, aPath, results)) { _.reduce(scopes, function (seenScopes, scope, index) { var sPath = aPath.concat(index.toString(), 'scope'); var sPtr = authPtr.concat(['scopes', scope.scope]); validateNoExist(seenScopes, scope.scope, code + '_SCOPE_REFERENCE', msgPrefix + ' scope reference', sPath, results.warnings); // Add reference or record unresolved authorization scope addReference(documentMetadata, sPtr, sPath, results); return seenScopes.concat(scope.scope); }, []); } return seenNames.concat(name); }, []); } else { _.reduce(authRefs, function (seenNames, scopes, index) { _.each(scopes, function (scopes, name) { var authPtr = ['securityDefinitions', name]; var authRefPath = path.concat(index.toString(), name); // Ensure the security definition isn't referenced more than once (Swagger 2.0+) validateNoExist(seenNames, name, code + '_REFERENCE', msgPrefix + ' reference', authRefPath, results.warnings); seenNames.push(name); // Add reference or record unresolved authorization if (addReference(documentMetadata, authPtr, authRefPath, results)) { _.each(scopes, function (scope, index) { // Add reference or record unresolved authorization scope var sPtr = authPtr.concat(['scopes', scope]); addReference(documentMetadata, sPtr, authRefPath.concat(index.toString()), results); }); } }); return seenNames; }, []); } }; var resolveRefs = function (apiDOrSO, callback) { var cacheEntry = getDocumentCache(apiDOrSO); var swaggerVersion = helpers.getSwaggerVersion(apiDOrSO); var jsonRefsOptions = { includeInvalid: true, loaderOptions: { processContent: function (res, callback) { callback(undefined, YAML.safeLoad(res.text)); } } }; if (!cacheEntry.resolved) { // For Swagger 1.2, we have to create real JSON References if (swaggerVersion === '1.2') { jsonRefsOptions.refPreProcessor = swagger1RefPreProcesor; } // Resolve references JsonRefs.resolveRefs(apiDOrSO, jsonRefsOptions) .then(function (results) { removeCirculars(results.resolved); // Fix circular references _.each(results.refs, function (refDetails, refPtr) { if (refDetails.circular) { _.set(results.resolved, JsonRefs.pathFromPtr(refPtr), {}); } }); cacheEntry.referencesMetadata = results.refs; cacheEntry.resolved = results.resolved; cacheEntry.resolvedId = SparkMD5.hash(JSON.stringify(results.resolved)); callback(); }) .catch(callback); } else { callback(); } }; var validateAgainstSchema = function (spec, schemaOrName, data, callback) { var validator = _.isString(schemaOrName) ? spec.validators[schemaOrName] : helpers.createJsonValidator(); helpers.registerCustomFormats(data); try { validators.validateAgainstSchema(schemaOrName, data, validator); } catch (err) { if (err.failedValidation) { return callback(undefined, err.results); } else { return callback(err); } } resolveRefs(data, function (err) { return callback(err); }); }; var validateDefinitions = function (documentMetadata, results) { // Validate unused definitions _.each(documentMetadata.definitions, function (metadata, id) { var defPath = JsonRefs.pathFromPtr(id); var defType = defPath[0].substring(0, defPath[0].length - 1); var displayId = documentMetadata.swaggerVersion === '1.2' ? defPath[defPath.length - 1] : id; var code = defType === 'securityDefinition' ? 'SECURITY_DEFINITION' : defType.toUpperCase(); var msgPrefix = defType === 'securityDefinition' ? 'Security definition' : defType.charAt(0).toUpperCase() + defType.substring(1); if (metadata.references.length === 0 && !metadata.inline) { // Swagger 1.2 authorization scope if (metadata.scopePath) { code += '_SCOPE'; msgPrefix += ' scope'; defPath = metadata.scopePath; } createUnusedErrorOrWarning(displayId, code, msgPrefix, defPath, results.warnings); } }); }; var validateParameters = function (spec, documentMetadata, nPath, parameters, path, results, skipMissing) { var createParameterComboError = function (path) { createErrorOrWarning('INVALID_PARAMETER_COMBINATION', 'API cannot have a a body parameter and a ' + (spec.version === '1.2' ? 'form' : 'formData') + ' parameter', path, results.errors); }; var pathParams = []; var seenBodyParam = false; var seenFormParam = false; _.reduce(parameters, function (seenParameters, parameter, index) { var pPath = path.concat(['parameters', index.toString()]); // Unresolved parameter if (_.isUndefined(parameter)) { return; } // Identify duplicate parameter names validateNoExist(seenParameters, parameter.name, 'PARAMETER', 'Parameter', pPath.concat('name'), results.errors); // Keep track of body and path parameters if (parameter.paramType === 'body' || parameter.in === 'body') { if (seenBodyParam === true) { createErrorOrWarning('DUPLICATE_API_BODY_PARAMETER', 'API has more than one body parameter', pPath, results.errors); } else if (seenFormParam === true) { createParameterComboError(pPath); } seenBodyParam = true; } else if (parameter.paramType === 'form' || parameter.in === 'formData') { if (seenBodyParam === true) { createParameterComboError(pPath); } seenFormParam = true; } else if (parameter.paramType === 'path' || parameter.in === 'path') { if (nPath.args.indexOf(parameter.name) === -1) { createErrorOrWarning('UNRESOLVABLE_API_PATH_PARAMETER', 'API path parameter could not be resolved: ' + parameter.name, pPath.concat('name'), results.errors); } pathParams.push(parameter.name); } if (spec.primitives.indexOf(parameter.type) === -1 && spec.version === '1.2') { addReference(documentMetadata, '#/models/' + parameter.type, pPath.concat('type'), results); } // Validate parameter constraints validateSchemaConstraints(documentMetadata, parameter, pPath, results, parameter.skipErrors); return seenParameters.concat(parameter.name); }, []); // Validate missing path parameters (in path but not in operation.parameters) if (_.isUndefined(skipMissing) || skipMissing === false) { _.each(_.difference(nPath.args, pathParams), function (unused) { createErrorOrWarning('MISSING_API_PATH_PARAMETER', 'API requires path parameter but it is not defined: ' + unused, documentMetadata.swaggerVersion === '1.2' ? path.slice(0, 2).concat('path') : path, results.errors); }); } }; var validateSwagger1_2 = function (spec, resourceListing, apiDeclarations, callback) { // jshint ignore:line var adResourcePaths = []; var rlDocumentMetadata = getDocumentCache(resourceListing); var rlResourcePaths = []; var results = { errors: [], warnings: [], apiDeclarations: [] }; // Process Resource Listing resource definitions rlResourcePaths = _.reduce(resourceListing.apis, function (seenPaths, api, index) { // Identify duplicate resource paths defined in the Resource Listing validateNoExist(seenPaths, api.path, 'RESOURCE_PATH', 'Resource path', ['apis', index.toString(), 'path'], results.errors); seenPaths.push(api.path); return seenPaths; }, []); // Process Resource Listing definitions (authorizations) processDocument(rlDocumentMetadata, results); // Process each API Declaration adResourcePaths = _.reduce(apiDeclarations, function (seenResourcePaths, apiDeclaration, index) { var aResults = results.apiDeclarations[index] = { errors: [], warnings: [] }; var adDocumentMetadata = getDocumentCache(apiDeclaration); // Identify duplicate resource paths defined in the API Declarations validateNoExist(seenResourcePaths, apiDeclaration.resourcePath, 'RESOURCE_PATH', 'Resource path', ['resourcePath'], aResults.errors); if (adResourcePaths.indexOf(apiDeclaration.resourcePath) === -1) { // Identify unused resource paths defined in the API Declarations validateExist(rlResourcePaths, apiDeclaration.resourcePath, 'RESOURCE_PATH', 'Resource path', ['resourcePath'], aResults.errors); seenResourcePaths.push(apiDeclaration.resourcePath); } // TODO: Process authorization references // Not possible due to https://github.com/swagger-api/swagger-spec/issues/159 // Process models processDocument(adDocumentMetadata, aResults); // Process the API definitions _.reduce(apiDeclaration.apis, function (seenPaths, api, index) { var aPath = ['apis', index.toString()]; var nPath = normalizePath(api.path); // Validate duplicate resource path if (seenPaths.indexOf(nPath.path) > -1) { createErrorOrWarning('DUPLICATE_API_PATH', 'API path (or equivalent) already defined: ' + api.path, aPath.concat('path'), aResults.errors); } else { seenPaths.push(nPath.path); } // Process the API operations _.reduce(api.operations, function (seenMethods, operation, index) { var oPath = aPath.concat(['operations', index.toString()]); // Validate duplicate operation method validateNoExist(seenMethods, operation.method, 'OPERATION_METHOD', 'Operation method', oPath.concat('method'), aResults.errors); // Keep track of the seen methods seenMethods.push(operation.method); // Keep track of operation types if (spec.primitives.indexOf(operation.type) === -1 && spec.version === '1.2') { addReference(adDocumentMetadata, '#/models/' + operation.type, oPath.concat('type'), aResults); } // Process authorization references processAuthRefs(rlDocumentMetadata, operation.authorizations, oPath.concat('authorizations'), aResults); // Validate validate inline constraints validateSchemaConstraints(adDocumentMetadata, operation, oPath, aResults); // Validate parameters validateParameters(spec, adDocumentMetadata, nPath, operation.parameters, oPath, aResults); // Validate unique response code _.reduce(operation.responseMessages, function (seenResponseCodes, responseMessage, index) { var rmPath = oPath.concat(['responseMessages', index.toString()]); validateNoExist(seenResponseCodes, responseMessage.code, 'RESPONSE_MESSAGE_CODE', 'Response message code', rmPath.concat(['code']), aResults.errors); // Validate missing model if (responseMessage.responseModel) { addReference(adDocumentMetadata, '#/models/' + responseMessage.responseModel, rmPath.concat('responseModel'), aResults); } return seenResponseCodes.concat(responseMessage.code); }, []); return seenMethods; }, []); return seenPaths; }, []); // Validate API Declaration definitions validateDefinitions(adDocumentMetadata, aResults); return seenResourcePaths; }, []); // Validate API Declaration definitions validateDefinitions(rlDocumentMetadata, results); // Identify unused resource paths defined in the Resource Listing _.each(_.difference(rlResourcePaths, adResourcePaths), function (unused) { var index = rlResourcePaths.indexOf(unused); createUnusedErrorOrWarning(resourceListing.apis[index].path, 'RESOURCE_PATH', 'Resource path', ['apis', index.toString(), 'path'], results.errors); }); callback(undefined, results); }; var validateSwagger2_0 = function (spec, swaggerObject, callback) { // jshint ignore:line var documentMetadata = getDocumentCache(swaggerObject); var results = { errors: [], warnings: [] }; // Process definitions processDocument(documentMetadata, results); // Process security references processAuthRefs(documentMetadata, swaggerObject.security, ['security'], results); _.reduce(documentMetadata.resolved.paths, function (seenPaths, path, name) { var pPath = ['paths', name]; var nPath = normalizePath(name); // Validate duplicate resource path if (seenPaths.indexOf(nPath.path) > -1) { createErrorOrWarning('DUPLICATE_API_PATH', 'API path (or equivalent) already defined: ' + name, pPath, results.errors); } // Validate parameters validateParameters(spec, documentMetadata, nPath, path.parameters, pPath, results, true); // Validate the Operations _.each(path, function (operation, method) { var cParams = []; var oPath = pPath.concat(method); var seenParams = []; if (validOptionNames.indexOf(method) === -1) { return; } // Process security references processAuthRefs(documentMetadata, operation.security, oPath.concat('security'), results); // Compose parameters from path global parameters and operation parameters _.each(operation.parameters, function (parameter) { // Can happen with invalid references if (_.isUndefined(parameter)) { return; } cParams.push(parameter); seenParams.push(parameter.name + ':' + parameter.in); }); _.each(path.parameters, function (parameter) { var cloned = _.cloneDeep(parameter); // The only errors that can occur here are schema constraint validation errors which are already reported above // so do not report them again. cloned.skipErrors = true; if (seenParams.indexOf(parameter.name + ':' + parameter.in) === -1) { cParams.push(cloned); } }); // Validate parameters validateParameters(spec, documentMetadata, nPath, cParams, oPath, results); // Validate responses _.each(operation.responses, function (response, responseCode) { // Do not process references to missing responses if (!_.isUndefined(response)) { // Validate validate inline constraints validateSchemaConstraints(documentMetadata, response, oPath.concat('responses', responseCode), results); } }); }); return seenPaths.concat(nPath.path); }, []); // Validate definitions validateDefinitions(documentMetadata, results); callback(undefined, results); }; var validateSemantically = function (spec, rlOrSO, apiDeclarations, callback) { var cbWrapper = function (err, results) { callback(err, helpers.formatResults(results)); }; if (spec.version === '1.2') { validateSwagger1_2(spec, rlOrSO, apiDeclarations, cbWrapper); // jshint ignore:line } else { validateSwagger2_0(spec, rlOrSO, cbWrapper); // jshint ignore:line } }; var validateStructurally = function (spec, rlOrSO, apiDeclarations, callback) { validateAgainstSchema(spec, spec.version === '1.2' ? 'resourceListing.json' : 'schema.json', rlOrSO, function (err, results) { if (err) { return callback(err); } // Only validate the API Declarations if the API is 1.2 and the Resource Listing was valid if (!results && spec.version === '1.2') { results = { errors: [], warnings: [], apiDeclarations: [] }; async.map(apiDeclarations, function (apiDeclaration, callback2) { validateAgainstSchema(spec, 'apiDeclaration.json', apiDeclaration, callback2); }, function (err, allResults) { if (err) { return callback(err); } _.each(allResults, function (result, index) { results.apiDeclarations[index] = result; }); callback(undefined, results); }); } else { callback(undefined, results); } }); }; /** * Creates a new Swagger specification object. * * @param {string} version - The Swagger version * * @constructor */ var Specification = function (version) { var that = this; var createValidators = function (spec, validatorsMap) { return _.reduce(validatorsMap, function (result, schemas, schemaName) { result[schemaName] = helpers.createJsonValidator(schemas); return result; }, {}); }; var fixSchemaId = function (schemaName) { // Swagger 1.2 schema files use one id but use a different id when referencing schema files. We also use the schema // file name to reference the schema in ZSchema. To fix this so that the JSON Schema validator works properly, we // need to set the id to be the name of the schema file. var fixed = _.cloneDeep(that.schemas[schemaName]); fixed.id = schemaName; return fixed; }; var primitives = ['string', 'number', 'boolean', 'integer', 'array']; switch (version) { case '1.2': this.docsUrl = 'https://github.com/swagger-api/swagger-spec/blob/master/versions/1.2.md'; this.primitives = _.union(primitives, ['void', 'File']); this.schemasUrl = 'https://github.com/swagger-api/swagger-spec/tree/master/schemas/v1.2'; // Here explicitly to allow browserify to work this.schemas = { 'apiDeclaration.json': require('../schemas/1.2/apiDeclaration.json'), 'authorizationObject.json': require('../schemas/1.2/authorizationObject.json'), 'dataType.json': require('../schemas/1.2/dataType.json'), 'dataTypeBase.json': require('../schemas/1.2/dataTypeBase.json'), 'infoObject.json': require('../schemas/1.2/infoObject.json'), 'modelsObject.json': require('../schemas/1.2/modelsObject.json'), 'oauth2GrantType.json': require('../schemas/1.2/oauth2GrantType.json'), 'operationObject.json': require('../schemas/1.2/operationObject.json'), 'parameterObject.json': require('../schemas/1.2/parameterObject.json'), 'resourceListing.json': require('../schemas/1.2/resourceListing.json'), 'resourceObject.json': require('../schemas/1.2/resourceObject.json') }; this.validators = createValidators(this, { 'apiDeclaration.json': _.map([ 'dataTypeBase.json', 'modelsObject.json', 'oauth2GrantType.json', 'authorizationObject.json', 'parameterObject.json', 'operationObject.json', 'apiDeclaration.json' ], fixSchemaId), 'resourceListing.json': _.map([ 'resourceObject.json', 'infoObject.json', 'oauth2GrantType.json', 'authorizationObject.json', 'resourceListing.json' ], fixSchemaId) }); break; case '2.0': this.docsUrl = 'https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md'; this.primitives = _.union(primitives, ['file']); this.schemasUrl = 'https://github.com/swagger-api/swagger-spec/tree/master/schemas/v2.0'; // Here explicitly to allow browserify to work this.schemas = { 'schema.json': require('../schemas/2.0/schema.json') }; this.validators = createValidators(this, { 'schema.json': [fixSchemaId('schema.json')] }); break; default: throw new Error(version + ' is an unsupported Swagger specification version'); } this.version = version; }; /** * Returns the result of the validation of the Swagger document(s). * * @param {object} rlOrSO - The Swagger Resource Listing (1.2) or Swagger Object (2.0) * @param {object[]} [apiDeclarations] - The array of Swagger API Declarations (1.2) * @param {resultCallback} callback - The result callback * * @returns undefined if validation passes or an object containing errors and/or warnings * @throws Error if the arguments provided are not valid */ Specification.prototype.validate = function (rlOrSO, apiDeclarations, callback) { // Validate arguments switch (this.version) { case '1.2': // Validate arguments if (_.isUndefined(rlOrSO)) { throw new Error('resourceListing is required'); } else if (!_.isPlainObject(rlOrSO)) { throw new TypeError('resourceListing must be an object'); } if (_.isUndefined(apiDeclarations)) { throw new Error('apiDeclarations is required'); } else if (!_.isArray(apiDeclarations)) { throw new TypeError('apiDeclarations must be an array'); } break; case '2.0': // Validate arguments if (_.isUndefined(rlOrSO)) { throw new Error('swaggerObject is required'); } else if (!_.isPlainObject(rlOrSO)) { throw new TypeError('swaggerObject must be an object'); } break; } if (this.version === '2.0') { callback = arguments[1]; } if (_.isUndefined(callback)) { throw new Error('callback is required'); } else if (!_.isFunction(callback)) { throw new TypeError('callback must be a function'); } // For Swagger 2.0, make sure apiDeclarations is an empty array if (this.version === '2.0') { apiDeclarations = []; } var that = this; // Perform the validation validateStructurally(this, rlOrSO, apiDeclarations, function (err, result) { if (err || helpers.formatResults(result)) { callback(err, result); } else { validateSemantically(that, rlOrSO, apiDeclarations, callback); } }); }; /** * Returns a JSON Schema representation of a composed model based on its id or reference. * * Note: For Swagger 1.2, we only perform structural validation prior to composing the model. * * @param {object} apiDOrSO - The Swagger Resource API Declaration (1.2) or the Swagger Object (2.0) * @param {string} modelIdOrRef - The model id (1.2) or the reference to the model (1.2 or 2.0) * @param {resultCallback} callback - The result callback * * @returns the object representing a composed object * * @throws Error if there are validation errors while creating */ Specification.prototype.composeModel = function (apiDOrSO, modelIdOrRef, callback) { var swaggerVersion = helpers.getSwaggerVersion(apiDOrSO); var doComposition = function (err, results) { var documentMetadata; if (err) { return callback(err); } else if (helpers.getErrorCount(results) > 0) { return handleValidationError(results, callback); } documentMetadata = getDocumentCache(apiDOrSO); results = { errors: [], warnings: [] }; processDocument(documentMetadata, results); if (!documentMetadata.definitions[modelIdOrRef]) { return callback(); } if (helpers.getErrorCount(results) > 0) { return handleValidationError(results, callback); } callback(undefined, getOrComposeSchema(documentMetadata, modelIdOrRef)); }; switch (this.version) { case '1.2': // Validate arguments if (_.isUndefined(apiDOrSO)) { throw new Error('apiDeclaration is required'); } else if (!_.isPlainObject(apiDOrSO)) { throw new TypeError('apiDeclaration must be an object'); } if (_.isUndefined(modelIdOrRef)) { throw new Error('modelId is required'); } break; case '2.0': // Validate arguments if (_.isUndefined(apiDOrSO)) { throw new Error('swaggerObject is required'); } else if (!_.isPlainObject(apiDOrSO)) { throw new TypeError('swaggerObject must be an object'); } if (_.isUndefined(modelIdOrRef)) { throw new Error('modelRef is required'); } break; } if (_.isUndefined(callback)) { throw new Error('callback is required'); } else if (!_.isFunction(callback)) { throw new TypeError('callback must be a function'); } if (modelIdOrRef.charAt(0) !== '#') { if (this.version === '1.2') { modelIdOrRef = '#/models/' + modelIdOrRef; } else { throw new Error('modelRef must be a JSON Pointer'); } } // Ensure the document is valid first if (swaggerVersion === '1.2') { validateAgainstSchema(this, 'apiDeclaration.json', apiDOrSO, doComposition); } else { this.validate(apiDOrSO, doComposition); } }; /** * Validates a model based on its id. * * Note: For Swagger 1.2, we only perform structural validation prior to composing the model. * * @param {object} apiDOrSO - The Swagger Resource API Declaration (1.2) or the Swagger Object (2.0) * @param {string} modelIdOrRef - The model id (1.2) or the reference to the model (1.2 or 2.0) * @param {*} data - The model to validate * @param {resultCallback} callback - The result callback * * @returns undefined if validation passes or an object containing errors and/or warnings * * @throws Error if there are validation errors while creating */ Specification.prototype.validateModel = function (apiDOrSO, modelIdOrRef, data, callback) { switch (this.version) { case '1.2': // Validate arguments if (_.isUndefined(apiDOrSO)) { throw new Error('apiDeclaration is required'); } else if (!_.isPlainObject(apiDOrSO)) { throw new TypeError('apiDeclaration must be an object'); } if (_.isUndefined(modelIdOrRef)) { throw new Error('modelId is required'); } break; case '2.0': // Validate arguments if (_.isUndefined(apiDOrSO)) { throw new Error('swaggerObject is required'); } else if (!_.isPlainObject(apiDOrSO)) { throw new TypeError('swaggerObject must be an object'); } if (_.isUndefined(modelIdOrRef)) { throw new Error('modelRef is required'); } break; } if (_.isUndefined(data)) { throw new Error('data is required'); } if (_.isUndefined(callback)) { throw new Error('callback is required'); } else if (!_.isFunction(callback)) { throw new TypeError('callback must be a function'); } var that = this; this.composeModel(apiDOrSO, modelIdOrRef, function (err, result) { if (err) { return callback(err); } validateAgainstSchema(that, result, data, callback); }); }; /** * Returns a fully resolved document or document fragment. (Does not perform validation as this is typically called * after validation occurs.)) * * @param {object} document - The document to resolve or the document containing the reference to resolve * @param {string} [ptr] - The JSON Pointer or undefined to return the whole document * @param {resultCallback} callback - The result callback * * @returns the fully resolved document or fragment * * @throws Error if there are upstream errors */ Specification.prototype.resolve = function (document, ptr, callback) { var documentMetadata; var respond = function (document) { if (_.isString(ptr)) { return callback(undefined, traverse(document).get(JsonRefs.pathFromPtr(ptr))); } else { return callback(undefined, document); } }; // Validate arguments if (_.isUndefined(document)) { throw new Error('document is required'); } else if (!_.isPlainObject(document)) { throw new TypeError('document must be an object'); } if (arguments.length === 2) { callback = arguments[1]; ptr = undefined; } if (!_.isUndefined(ptr) && !_.isString(ptr)) { throw new TypeError('ptr must be a JSON Pointer string'); } if (_.isUndefined(callback)) { throw new Error('callback is required'); } else if (!_.isFunction(callback)) { throw new TypeError('callback must be a function'); } documentMetadata = getDocumentCache(document); // Swagger 1.2 is not supported due to invalid JSON References being used. Even if the JSON References were valid, // the JSON Schema for Swagger 1.2 do not allow JavaScript objects in all places where the resoution would occur. if (documentMetadata.swaggerVersion === '1.2') { throw new Error('Swagger 1.2 is not supported'); } if (!documentMetadata.resolved) { // Ensure the document is valid first this.validate(document, function (err, results) { if (err) { return callback(err); } else if (helpers.getErrorCount(results) > 0) { return handleValidationError(results, callback); } return respond(documentMetadata.resolved); }); } else { return respond(documentMetadata.resolved); } }; /** * Converts the Swagger 1.2 documents to a Swagger 2.0 document. * * @param {object} resou