swagger-tools
Version:
Various tools for using and integrating with Swagger.
672 lines (574 loc) • 20.8 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.
*/
;
// Done this way to make the Browserify build smaller
var _ = {
cloneDeep: require('lodash-compat/lang/cloneDeep'),
each: require('lodash-compat/collection/each'),
isArray: require('lodash-compat/lang/isArray'),
isBoolean: require('lodash-compat/lang/isBoolean'),
isDate: require('lodash-compat/lang/isDate'),
isFinite: require('lodash-compat/lang/isFinite'),
isNull: require('lodash-compat/lang/isNull'),
isNumber: require('lodash-compat/lang/isNumber'),
isPlainObject: require('lodash-compat/lang/isPlainObject'),
isString: require('lodash-compat/lang/isString'),
isUndefined: require('lodash-compat/lang/isUndefined'),
map: require('lodash-compat/collection/map'),
union: require('lodash-compat/array/union'),
uniq: require('lodash-compat/array/uniq')
};
var helpers = require('./helpers');
// http://tools.ietf.org/html/rfc3339#section-5.6
var dateRegExp = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/;
// http://tools.ietf.org/html/rfc3339#section-5.6
var dateTimeRegExp = /^([0-9]{2}):([0-9]{2}):([0-9]{2})(.[0-9]+)?(z|([+-][0-9]{2}:[0-9]{2}))$/;
var isValidDate = function (date) {
var day;
var matches;
var month;
if (_.isDate(date)) {
return true;
}
if (!_.isString(date)) {
date = date.toString();
}
matches = dateRegExp.exec(date);
if (matches === null) {
return false;
}
day = matches[3];
month = matches[2];
if (month < '01' || month > '12' || day < '01' || day > '31') {
return false;
}
return true;
};
var isValidDateTime = function (dateTime) {
var hour;
var date;
var time;
var matches;
var minute;
var parts;
var second;
if (_.isDate(dateTime)) {
return true;
}
if (!_.isString(dateTime)) {
dateTime = dateTime.toString();
}
parts = dateTime.toLowerCase().split('t');
date = parts[0];
time = parts.length > 1 ? parts[1] : undefined;
if (!isValidDate(date)) {
return false;
}
matches = dateTimeRegExp.exec(time);
if (matches === null) {
return false;
}
hour = matches[1];
minute = matches[2];
second = matches[3];
if (hour > '23' || minute > '59' || second > '59') {
return false;
}
return true;
};
var throwErrorWithCode = function (code, msg) {
var err = new Error(msg);
err.code = code;
err.failedValidation = true;
throw err;
};
module.exports.validateAgainstSchema = function (schemaOrName, data, validator) {
var sanitizeError = function (obj) {
// Make anyOf/oneOf errors more human readable (Issue 200)
var defType = ['additionalProperties', 'items'].indexOf(obj.path[obj.path.length - 1]) > -1 ?
'schema' :
obj.path[obj.path.length - 2];
if (['ANY_OF_MISSING', 'ONE_OF_MISSING'].indexOf(obj.code) > -1) {
switch (defType) {
case 'parameters':
defType = 'parameter';
break;
case 'responses':
defType = 'response';
break;
case 'schema':
defType += ' ' + obj.path[obj.path.length - 1];
// no default
}
obj.message = 'Not a valid ' + defType + ' definition';
}
// Remove the params portion of the error
delete obj.params;
if (obj.inner) {
_.each(obj.inner, function (nObj) {
sanitizeError(nObj);
});
}
};
var schema = _.isPlainObject(schemaOrName) ? _.cloneDeep(schemaOrName) : schemaOrName;
// We don't check this due to internal usage but if validator is not provided, schemaOrName must be a schema
if (_.isUndefined(validator)) {
validator = helpers.createJsonValidator([schema]);
}
var valid = validator.validate(data, schema);
if (!valid) {
try {
throwErrorWithCode('SCHEMA_VALIDATION_FAILED', 'Failed schema validation');
} catch (err) {
err.results = {
errors: _.map(validator.getLastErrors(), function (err) {
sanitizeError(err);
return err;
}),
warnings: []
};
throw err;
}
}
};
/**
* Validates a schema of type array is properly formed (when necessar).
*
* *param {object} schema - The schema object to validate
*
* @throws Error if the schema says it's an array but it is not formed properly
*
* @see {@link https://github.com/swagger-api/swagger-spec/issues/174}
*/
var validateArrayType = module.exports.validateArrayType = function (schema) {
// We have to do this manually for now
if (schema.type === 'array' && _.isUndefined(schema.items)) {
throwErrorWithCode('OBJECT_MISSING_REQUIRED_PROPERTY', 'Missing required property: items');
}
};
/**
* Validates the request or response content type (when necessary).
*
* @param {string[]} gPOrC - The valid consumes at the API scope
* @param {string[]} oPOrC - The valid consumes at the operation scope
* @param {object} reqOrRes - The request or response
*
* @throws Error if the content type is invalid
*/
module.exports.validateContentType = function (gPOrC, oPOrC, reqOrRes) {
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
var isResponse = typeof reqOrRes.end === 'function';
var contentType = isResponse ? reqOrRes.getHeader('content-type') : reqOrRes.headers['content-type'];
var pOrC = _.union(gPOrC, oPOrC);
if (!contentType) {
if (isResponse) {
contentType = 'text/plain';
} else {
contentType = 'application/octet-stream';
}
}
// Get only the content type
contentType = contentType.split(';')[0];
if (pOrC.length > 0 && (isResponse ?
true :
['POST', 'PUT'].indexOf(reqOrRes.method) !== -1) && pOrC.indexOf(contentType) === -1) {
throw new Error('Invalid content type (' + contentType + '). These are valid: ' + pOrC.join(', '));
}
};
/**
* Validates the value against the allowable values (when necessary).
*
* @param {*} val - The parameter value
* @param {string[]} allowed - The allowable values
*
* @throws Error if the value is not allowable
*/
var validateEnum = module.exports.validateEnum = function (val, allowed) {
if (!_.isUndefined(allowed) && !_.isUndefined(val) && allowed.indexOf(val) === -1) {
throwErrorWithCode('ENUM_MISMATCH', 'Not an allowable value (' + allowed.join(', ') + '): ' + val);
}
};
/**
* Validates the value is less than the maximum (when necessary).
*
* @param {*} val - The parameter value
* @param {string} maximum - The maximum value
* @param {boolean} [exclusive=false] - Whether or not the value includes the maximum in its comparison
*
* @throws Error if the value is greater than the maximum
*/
var validateMaximum = module.exports.validateMaximum = function (val, maximum, type, exclusive) {
var code = exclusive === true ? 'MAXIMUM_EXCLUSIVE' : 'MAXIMUM';
var testMax;
var testVal;
if (_.isUndefined(exclusive)) {
exclusive = false;
}
if (type === 'integer') {
testVal = parseInt(val, 10);
} else if (type === 'number') {
testVal = parseFloat(val);
}
if (!_.isUndefined(maximum)) {
testMax = parseFloat(maximum);
if (exclusive && testVal >= testMax) {
throwErrorWithCode(code, 'Greater than or equal to the configured maximum (' + maximum + '): ' + val);
} else if (testVal > testMax) {
throwErrorWithCode(code, 'Greater than the configured maximum (' + maximum + '): ' + val);
}
}
};
/**
* Validates the array count is less than the maximum (when necessary).
*
* @param {*[]} val - The parameter value
* @param {number} maxItems - The maximum number of items
*
* @throws Error if the value contains more items than allowable
*/
var validateMaxItems = module.exports.validateMaxItems = function (val, maxItems) {
if (!_.isUndefined(maxItems) && val.length > maxItems) {
throwErrorWithCode('ARRAY_LENGTH_LONG', 'Array is too long (' + val.length + '), maximum ' + maxItems);
}
};
/**
* Validates the value length is less than the maximum (when necessary).
*
* @param {*[]} val - The parameter value
* @param {number} maxLength - The maximum length
*
* @throws Error if the value's length is greater than the maximum
*/
var validateMaxLength = module.exports.validateMaxLength = function (val, maxLength) {
if (!_.isUndefined(maxLength) && val.length > maxLength) {
throwErrorWithCode('MAX_LENGTH', 'String is too long (' + val.length + ' chars), maximum ' + maxLength);
}
};
/**
* Validates the value's property count is greater than the maximum (when necessary).
*
* @param {*[]} val - The parameter value
* @param {number} minProperties - The maximum number of properties
*
* @throws Error if the value's property count is less than the maximum
*/
var validateMaxProperties = module.exports.validateMaxProperties = function (val, maxProperties) {
var propCount = _.isPlainObject(val) ? Object.keys(val).length : 0;
if (!_.isUndefined(maxProperties) && propCount > maxProperties) {
throwErrorWithCode('MAX_PROPERTIES',
'Number of properties is too many (' + propCount + ' properties), maximum ' + maxProperties);
}
};
/**
* Validates the value array count is greater than the minimum (when necessary).
*
* @param {*} val - The parameter value
* @param {string} minimum - The minimum value
* @param {boolean} [exclusive=false] - Whether or not the value includes the minimum in its comparison
*
* @throws Error if the value is less than the minimum
*/
var validateMinimum = module.exports.validateMinimum = function (val, minimum, type, exclusive) {
var code = exclusive === true ? 'MINIMUM_EXCLUSIVE' : 'MINIMUM';
var testMin;
var testVal;
if (_.isUndefined(exclusive)) {
exclusive = false;
}
if (type === 'integer') {
testVal = parseInt(val, 10);
} else if (type === 'number') {
testVal = parseFloat(val);
}
if (!_.isUndefined(minimum)) {
testMin = parseFloat(minimum);
if (exclusive && testVal <= testMin) {
throwErrorWithCode(code, 'Less than or equal to the configured minimum (' + minimum + '): ' + val);
} else if (testVal < testMin) {
throwErrorWithCode(code, 'Less than the configured minimum (' + minimum + '): ' + val);
}
}
};
/**
* Validates the value value contains fewer items than allowed (when necessary).
*
* @param {*[]} val - The parameter value
* @param {number} minItems - The minimum number of items
*
* @throws Error if the value contains fewer items than allowable
*/
var validateMinItems = module.exports.validateMinItems = function (val, minItems) {
if (!_.isUndefined(minItems) && val.length < minItems) {
throwErrorWithCode('ARRAY_LENGTH_SHORT', 'Array is too short (' + val.length + '), minimum ' + minItems);
}
};
/**
* Validates the value length is less than the minimum (when necessary).
*
* @param {*[]} val - The parameter value
* @param {number} minLength - The minimum length
*
* @throws Error if the value's length is less than the minimum
*/
var validateMinLength = module.exports.validateMinLength = function (val, minLength) {
if (!_.isUndefined(minLength) && val.length < minLength) {
throwErrorWithCode('MIN_LENGTH', 'String is too short (' + val.length + ' chars), minimum ' + minLength);
}
};
/**
* Validates the value's property count is less than or equal to the minimum (when necessary).
*
* @param {*[]} val - The parameter value
* @param {number} minProperties - The minimum number of properties
*
* @throws Error if the value's property count is less than the minimum
*/
var validateMinProperties = module.exports.validateMinProperties = function (val, minProperties) {
var propCount = _.isPlainObject(val) ? Object.keys(val).length : 0;
if (!_.isUndefined(minProperties) && propCount < minProperties) {
throwErrorWithCode('MIN_PROPERTIES',
'Number of properties is too few (' + propCount + ' properties), minimum ' + minProperties);
}
};
/**
* Validates the value is a multiple of the provided number (when necessary).
*
* @param {*[]} val - The parameter value
* @param {number} multipleOf - The number that should divide evenly into the value
*
* @throws Error if the value contains fewer items than allowable
*/
var validateMultipleOf = module.exports.validateMultipleOf = function (val, multipleOf) {
if (!_.isUndefined(multipleOf) && val % multipleOf !== 0) {
throwErrorWithCode('MULTIPLE_OF', 'Not a multiple of ' + multipleOf);
}
};
/**
* Validates the value matches a pattern (when necessary).
*
* @param {string} name - The parameter name
* @param {*} val - The parameter value
* @param {string} pattern - The pattern
*
* @throws Error if the value does not match the pattern
*/
var validatePattern = module.exports.validatePattern = function (val, pattern) {
if (!_.isUndefined(pattern) && _.isNull(val.match(new RegExp(pattern)))) {
throwErrorWithCode('PATTERN', 'Does not match required pattern: ' + pattern);
}
};
/**
* Validates the value requiredness (when necessary).
*
* @param {*} val - The parameter value
* @param {boolean} required - Whether or not the parameter is required
*
* @throws Error if the value is required but is not present
*/
module.exports.validateRequiredness = function (val, required) {
if (!_.isUndefined(required) && required === true && _.isUndefined(val)) {
throwErrorWithCode('REQUIRED', 'Is required');
}
};
/**
* Validates the value type and format (when necessary).
*
* @param {string} version - The Swagger version
* @param {*} val - The parameter value
* @param {string} type - The parameter type
* @param {string} format - The parameter format
* @param {boolean} [skipError=false] - Whether or not to skip throwing an error (Useful for validating arrays)
*
* @throws Error if the value is not the proper type or format
*/
var validateTypeAndFormat = module.exports.validateTypeAndFormat =
function validateTypeAndFormat (version, val, type, format, allowEmptyValue, skipError) {
var result = true;
var oVal = val;
// If there is an empty value and we allow empty values, the value is always valid
if (allowEmptyValue === true && val === '') {
return;
}
if (_.isArray(val)) {
_.each(val, function (aVal, index) {
if (!validateTypeAndFormat(version, aVal, type, format, allowEmptyValue, true)) {
throwErrorWithCode('INVALID_TYPE', 'Value at index ' + index + ' is not a valid ' + type + ': ' + aVal);
}
});
} else {
switch (type) {
case 'boolean':
// Coerce the value only for Swagger 1.2
if (version === '1.2' && _.isString(val)) {
if (val === 'false') {
val = false;
} else if (val === 'true') {
val = true;
}
}
result = _.isBoolean(val);
break;
case 'integer':
// Coerce the value only for Swagger 1.2
if (version === '1.2' && _.isString(val)) {
val = Number(val);
}
result = _.isFinite(val) && (Math.round(val) === val);
break;
case 'number':
// Coerce the value only for Swagger 1.2
if (version === '1.2' && _.isString(val)) {
val = Number(val);
}
result = _.isFinite(val);
break;
case 'string':
if (!_.isUndefined(format)) {
switch (format) {
case 'date':
result = isValidDate(val);
break;
case 'date-time':
result = isValidDateTime(val);
break;
}
}
break;
case 'void':
result = _.isUndefined(val);
break;
}
}
if (skipError) {
return result;
} else if (!result) {
throwErrorWithCode('INVALID_TYPE',
type !== 'void' ?
'Not a valid ' + (_.isUndefined(format) ? '' : format + ' ') + type + ': ' + oVal :
'Void does not allow a value');
}
};
/**
* Validates the value values are unique (when necessary).
*
* @param {string[]} val - The parameter value
* @param {boolean} isUnique - Whether or not the parameter values are unique
*
* @throws Error if the value has duplicates
*/
var validateUniqueItems = module.exports.validateUniqueItems = function (val, isUnique) {
if (!_.isUndefined(isUnique) && _.uniq(val).length !== val.length) {
throwErrorWithCode('ARRAY_UNIQUE', 'Does not allow duplicate values: ' + val.join(', '));
}
};
/**
* Validates the value against the schema.
*
* @param {string} swaggerVersion - The Swagger version
* @param {object} schema - The schema to use to validate things
* @param {string[]} path - The path to the schema
* @param {*} [val] - The value to validate or undefined to use the default value provided by the schema
*
* @throws Error if any validation failes
*/
module.exports.validateSchemaConstraints = function (swaggerVersion, schema, path, val) {
var resolveSchema = function (schema) {
var resolved = schema;
if (resolved.schema) {
path = path.concat(['schema']);
resolved = resolveSchema(resolved.schema);
}
return resolved;
};
var type = schema.type;
var allowEmptyValue;
if (!type) {
if (!schema.schema) {
if (path[path.length - 2] === 'responses') {
type = 'void';
} else {
type = 'object';
}
} else {
schema = resolveSchema(schema);
type = schema.type || 'object';
}
}
allowEmptyValue = schema ? schema.allowEmptyValue === true : false;
try {
// Always perform this check even if there is no value
if (type === 'array') {
validateArrayType(schema);
}
// Default to default value if necessary
if (_.isUndefined(val)) {
val = swaggerVersion === '1.2' ? schema.defaultValue : schema.default;
path = path.concat([swaggerVersion === '1.2' ? 'defaultValue' : 'default']);
}
// If there is no explicit default value, return as all validations will fail
if (_.isUndefined(val)) {
return;
}
if (type === 'array') {
if (!_.isUndefined(schema.items)) {
validateTypeAndFormat(swaggerVersion, val, type === 'array' ? schema.items.type : type,
type === 'array' && schema.items.format ?
schema.items.format :
schema.format,
allowEmptyValue);
} else {
validateTypeAndFormat(swaggerVersion, val, type, schema.format, allowEmptyValue);
}
} else {
validateTypeAndFormat(swaggerVersion, val, type, schema.format, allowEmptyValue);
}
// Validate enum
validateEnum(val, schema.enum);
// Validate maximum
validateMaximum(val, schema.maximum, type, schema.exclusiveMaximum);
// Validate maxItems (Swagger 2.0+)
validateMaxItems(val, schema.maxItems);
// Validate maxLength (Swagger 2.0+)
validateMaxLength(val, schema.maxLength);
// Validate maxProperties (Swagger 2.0+)
validateMaxProperties(val, schema.maxProperties);
// Validate minimum
validateMinimum(val, schema.minimum, type, schema.exclusiveMinimum);
// Validate minItems
validateMinItems(val, schema.minItems);
// Validate minLength (Swagger 2.0+)
validateMinLength(val, schema.minLength);
// Validate minProperties (Swagger 2.0+)
validateMinProperties(val, schema.minProperties);
// Validate multipleOf (Swagger 2.0+)
validateMultipleOf(val, schema.multipleOf);
// Validate pattern (Swagger 2.0+)
validatePattern(val, schema.pattern);
// Validate uniqueItems
validateUniqueItems(val, schema.uniqueItems);
} catch (err) {
err.path = path;
throw err;
}
};