UNPKG

swagger-tools

Version:

Various tools for using and integrating with Swagger.

672 lines (574 loc) 20.8 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'; // 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; } };