swagger-tools
Version:
Various tools for using and integrating with Swagger.
1,448 lines (1,189 loc) • 52.5 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.
*/
'use strict';
// Done this way to make the Browserify build smaller
var _ = {
cloneDeep: require('lodash-compat/lang/cloneDeep'),
difference: require('lodash-compat/array/difference'),
each: require('lodash-compat/collection/each'),
find: require('lodash-compat/collection/find'),
has: require('lodash-compat/object/has'),
isArray: require('lodash-compat/lang/isArray'),
isFunction: require('lodash-compat/lang/isFunction'),
isPlainObject: require('lodash-compat/lang/isPlainObject'),
isString: require('lodash-compat/lang/isString'),
isUndefined: require('lodash-compat/lang/isUndefined'),
map: require('lodash-compat/collection/map'),
reduce: require('lodash-compat/collection/reduce'),
union: require('lodash-compat/array/union')
};
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 validOptionNames = _.map(helpers.swaggerOperationMethods, function (method) {
return method.toLowerCase();
});
var addExternalRefsToValidator = function (validator, json, callback) {
var remoteRefs = _.reduce(JsonRefs.findRefs(json), function (rRefs, ref, ptr) {
if (JsonRefs.isRemotePointer(ptr)) {
rRefs.push(ref.split('#')[0]);
}
return rRefs;
}, []);
var resolveRemoteRefs = function (ref, callback) {
JsonRefs.resolveRefs({$ref: ref}, function (err, json) {
if (err) {
return callback(err);
}
// Perform the same for the newly resolved document
addExternalRefsToValidator(validator, json, function (err, rJson) {
callback(err, rJson);
});
});
};
if (remoteRefs.length > 0) {
async.map(remoteRefs, resolveRemoteRefs, function (err, results) {
if (err) {
return callback(err);
}
_.each(results, function (json, index) {
validator.setRemoteReference(remoteRefs[index], json);
helpers.registerCustomFormats(validator, json);
});
callback();
});
} else {
helpers.registerCustomFormats(json);
callback();
}
};
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.pathFromPointer(defPathOrPtr);
var defPtr = _.isArray(defPathOrPtr) ? JsonRefs.pathToPointer(defPathOrPtr) : defPathOrPtr;
var refPath = _.isArray(refPathOrPtr) ? refPathOrPtr : JsonRefs.pathFromPointer(refPathOrPtr);
var refPtr = _.isArray(refPathOrPtr) ? JsonRefs.pathToPointer(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.pathToPointer(pPath);
var pDef = cacheEntry.definitions[pPtr];
if (!_.isUndefined(pDef)) {
def = pDef;
break;
}
}
}
if (_.isUndefined(def)) {
if (!omitError) {
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.pathFromPointer(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.pathFromPointer(modelId)));
composed = _.cloneDeep(resolvedT.get(JsonRefs.pathFromPointer(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), function (ref, ptr) {
var modelId = '#/models/' + ref;
var dMetadata = documentMetadata.definitions[modelId];
var path = JsonRefs.pathFromPointer(ptr);
if (dMetadata.lineage.length > 0) {
traverse(property).set(path.slice(0, path.length - 1), getOrComposeSchema(documentMetadata, modelId));
} else {
traverse(property).set(path.slice(0, path.length - 1).concat('title'), 'Composed ' + ref);
}
});
});
}
// 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 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.pathToPointer(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.pathFromPointer(defPtr)[0]) > -1) {
metadata.cyclical = false;
metadata.lineage = undefined;
metadata.parents = [];
}
}
return metadata;
};
var getDisplayId = function (id) {
return swaggerVersion === '1.2' ? JsonRefs.pathFromPointer(id).pop() : id;
};
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;
}, []);
});
// Proces 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.pathToPointer(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.pathToPointer(modelDefPath));
}
});
break;
default:
_.each(documentMetadata.original[modelDefsProp][modelId].allOf, function (schema, index) {
var isInline = false;
var parentPath;
if (_.isUndefined(schema.$ref) || JsonRefs.isRemotePointer(schema.$ref)) {
isInline = true;
parentPath = modelDefPath.concat(['allOf', index.toString()]);
} else {
parentPath = JsonRefs.pathFromPointer(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.pathToPointer(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.pathFromPointer(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.pathFromPointer(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);
}
});
});
// Process local references
_.each(JsonRefs.findRefs(documentMetadata.original), function (ref, refPtr) {
if (documentMetadata.swaggerVersion === '1.2') {
ref = '#/models/' + ref;
}
// Only process local references
if (!JsonRefs.isRemotePointer(ref)) {
addReference(documentMetadata, ref, refPtr, results);
}
});
// Process remote references
_.each(documentMetadata.referencesMetadata, function (details, ref) {
if (JsonRefs.isRemotePointer(details.ref) && !_.has(details, 'value')) {
results.errors.push({
code: 'UNRESOLVABLE_REFERENCE',
message: 'Reference could not be resolved: ' + details.ref,
path: JsonRefs.pathFromPointer(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 documentT;
if (!cacheEntry.resolved) {
// For Swagger 1.2, we have to create real JSON References
if (swaggerVersion === '1.2') {
apiDOrSO = _.cloneDeep(apiDOrSO);
documentT = traverse(apiDOrSO);
_.each(JsonRefs.findRefs(apiDOrSO), function (ref, ptr) {
// All Swagger 1.2 references are ALWAYS to models
documentT.set(JsonRefs.pathFromPointer(ptr), '#/models/' + ref);
});
}
// Resolve references
JsonRefs.resolveRefs(apiDOrSO, {
processContent: function (content) {
return YAML.safeLoad(content);
}
}, function (err, json, metadata) {
if (err) {
return callback(err);
}
cacheEntry.referencesMetadata = metadata;
cacheEntry.resolved = json;
cacheEntry.resolvedId = SparkMD5.hash(JSON.stringify(json));
callback();
});
} else {
callback();
}
};
var validateAgainstSchema = function (spec, schemaOrName, data, callback) {
var validator = _.isString(schemaOrName) ? spec.validators[schemaOrName] : helpers.createJsonValidator();
var doValidation = function () {
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);
});
};
addExternalRefsToValidator(validator, data, function (err) {
if (err) {
return callback(err);
}
helpers.registerCustomFormats(data);
doValidation();
});
};
var validateDefinitions = function (documentMetadata, results) {
// Validate unused definitions
_.each(documentMetadata.definitions, function (metadata, id) {
var defPath = JsonRefs.pathFromPointer(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, callback) {
validateAgainstSchema(spec, 'apiDeclaration.json', apiDeclaration, callback);
}, 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 {object} 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.pathFromPointer(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(do