"use strict"; const util = require("../util"); const { ono } = require("@jsdevtools/ono"); const swaggerMethods = require("@apidevtools/swagger-methods"); const primitiveTypes = ["array", "boolean", "integer", "number", "string"]; const schemaTypes = ["array", "boolean", "integer", "number", "string", "object", "null", undefined]; module.exports = validateSpec; /** * Validates parts of the Swagger 2.0 spec that aren't covered by the Swagger 2.0 JSON Schema. * * @param {SwaggerObject} api */ function validateSpec (api) { if (api.openapi) { // We don't (yet) support validating against the OpenAPI spec return; } let paths = Object.keys(api.paths || {}); let operationIds = []; for (let pathName of paths) { let path = api.paths[pathName]; let pathId = "/paths" + pathName; if (path && pathName.indexOf("/") === 0) { validatePath(api, path, pathId, operationIds); } } let definitions = Object.keys(api.definitions || {}); for (let definitionName of definitions) { let definition = api.definitions[definitionName]; let definitionId = "/definitions/" + definitionName; validateRequiredPropertiesExist(definition, definitionId); } } /** * Validates the given path. * * @param {SwaggerObject} api - The entire Swagger API object * @param {object} path - A Path object, from the Swagger API * @param {string} pathId - A value that uniquely identifies the path * @param {string} operationIds - An array of collected operationIds found in other paths */ function validatePath (api, path, pathId, operationIds) { for (let operationName of swaggerMethods) { let operation = path[operationName]; let operationId = pathId + "/" + operationName; if (operation) { let declaredOperationId = operation.operationId; if (declaredOperationId) { if (operationIds.indexOf(declaredOperationId) === -1) { operationIds.push(declaredOperationId); } else { throw ono.syntax(`Validation failed. Duplicate operation id '${declaredOperationId}'`); } } validateParameters(api, path, pathId, operation, operationId); let responses = Object.keys(operation.responses || {}); for (let responseName of responses) { let response = operation.responses[responseName]; let responseId = operationId + "/responses/" + responseName; validateResponse(responseName, (response || {}), responseId); } } } } /** * Validates the parameters for the given operation. * * @param {SwaggerObject} api - The entire Swagger API object * @param {object} path - A Path object, from the Swagger API * @param {string} pathId - A value that uniquely identifies the path * @param {object} operation - An Operation object, from the Swagger API * @param {string} operationId - A value that uniquely identifies the operation */ function validateParameters (api, path, pathId, operation, operationId) { let pathParams = path.parameters || []; let operationParams = operation.parameters || []; // Check for duplicate path parameters try { checkForDuplicates(pathParams); } catch (e) { throw ono.syntax(e, `Validation failed. ${pathId} has duplicate parameters`); } // Check for duplicate operation parameters try { checkForDuplicates(operationParams); } catch (e) { throw ono.syntax(e, `Validation failed. ${operationId} has duplicate parameters`); } // Combine the path and operation parameters, // with the operation params taking precedence over the path params let params = pathParams.reduce((combinedParams, value) => { let duplicate = combinedParams.some((param) => { return param.in === value.in && param.name === value.name; }); if (!duplicate) { combinedParams.push(value); } return combinedParams; }, operationParams.slice()); validateBodyParameters(params, operationId); validatePathParameters(params, pathId, operationId); validateParameterTypes(params, api, operation, operationId); } /** * Validates body and formData parameters for the given operation. * * @param {object[]} params - An array of Parameter objects * @param {string} operationId - A value that uniquely identifies the operation */ function validateBodyParameters (params, operationId) { let bodyParams = params.filter((param) => { return param.in === "body"; }); let formParams = params.filter((param) => { return param.in === "formData"; }); // There can only be one "body" parameter if (bodyParams.length > 1) { throw ono.syntax( `Validation failed. ${operationId} has ${bodyParams.length} body parameters. Only one is allowed.`, ); } else if (bodyParams.length > 0 && formParams.length > 0) { // "body" params and "formData" params are mutually exclusive throw ono.syntax( `Validation failed. ${operationId} has body parameters and formData parameters. Only one or the other is allowed.`, ); } } /** * Validates path parameters for the given path. * * @param {object[]} params - An array of Parameter objects * @param {string} pathId - A value that uniquely identifies the path * @param {string} operationId - A value that uniquely identifies the operation */ function validatePathParameters (params, pathId, operationId) { // Find all {placeholders} in the path string let placeholders = pathId.match(util.swaggerParamRegExp) || []; // Check for duplicates for (let i = 0; i < placeholders.length; i++) { for (let j = i + 1; j < placeholders.length; j++) { if (placeholders[i] === placeholders[j]) { throw ono.syntax( `Validation failed. ${operationId} has multiple path placeholders named ${placeholders[i]}`); } } } params = params.filter((param) => { return param.in === "path"; }); for (let param of params) { if (param.required !== true) { throw ono.syntax( "Validation failed. Path parameters cannot be optional. " + `Set required=true for the "${param.name}" parameter at ${operationId}`, ); } let match = placeholders.indexOf("{" + param.name + "}"); if (match === -1) { throw ono.syntax( `Validation failed. ${operationId} has a path parameter named "${param.name}", ` + `but there is no corresponding {${param.name}} in the path string` ); } placeholders.splice(match, 1); } if (placeholders.length > 0) { throw ono.syntax(`Validation failed. ${operationId} is missing path parameter(s) for ${placeholders}`); } } /** * Validates data types of parameters for the given operation. * * @param {object[]} params - An array of Parameter objects * @param {object} api - The entire Swagger API object * @param {object} operation - An Operation object, from the Swagger API * @param {string} operationId - A value that uniquely identifies the operation */ function validateParameterTypes (params, api, operation, operationId) { for (let param of params) { let parameterId = operationId + "/parameters/" + param.name; let schema, validTypes; switch (param.in) { case "body": schema = param.schema; validTypes = schemaTypes; break; case "formData": schema = param; validTypes = primitiveTypes.concat("file"); break; default: schema = param; validTypes = primitiveTypes; } validateSchema(schema, parameterId, validTypes); validateRequiredPropertiesExist(schema, parameterId); if (schema.type === "file") { // "file" params must consume at least one of these MIME types let formData = /multipart\/(.*\+)?form-data/; let urlEncoded = /application\/(.*\+)?x-www-form-urlencoded/; let consumes = operation.consumes || api.consumes || []; let hasValidMimeType = consumes.some((consume) => { return formData.test(consume) || urlEncoded.test(consume); }); if (!hasValidMimeType) { throw ono.syntax( `Validation failed. ${operationId} has a file parameter, so it must consume multipart/form-data ` + "or application/x-www-form-urlencoded", ); } } } } /** * Checks the given parameter list for duplicates, and throws an error if found. * * @param {object[]} params - An array of Parameter objects */ function checkForDuplicates (params) { for (let i = 0; i < params.length - 1; i++) { let outer = params[i]; for (let j = i + 1; j < params.length; j++) { let inner = params[j]; if (outer.name === inner.name && outer.in === inner.in) { throw ono.syntax(`Validation failed. Found multiple ${outer.in} parameters named "${outer.name}"`); } } } } /** * Validates the given response object. * * @param {string} code - The HTTP response code (or "default") * @param {object} response - A Response object, from the Swagger API * @param {string} responseId - A value that uniquely identifies the response */ function validateResponse (code, response, responseId) { if (code !== "default" && (code < 100 || code > 599)) { throw ono.syntax(`Validation failed. ${responseId} has an invalid response code (${code})`); } let headers = Object.keys(response.headers || {}); for (let headerName of headers) { let header = response.headers[headerName]; let headerId = responseId + "/headers/" + headerName; validateSchema(header, headerId, primitiveTypes); } if (response.schema) { let validTypes = schemaTypes.concat("file"); if (validTypes.indexOf(response.schema.type) === -1) { throw ono.syntax( `Validation failed. ${responseId} has an invalid response schema type (${response.schema.type})`); } else { validateSchema(response.schema, responseId + "/schema", validTypes); } } } /** * Validates the given Swagger schema object. * * @param {object} schema - A Schema object, from the Swagger API * @param {string} schemaId - A value that uniquely identifies the schema object * @param {string[]} validTypes - An array of the allowed schema types */ function validateSchema (schema, schemaId, validTypes) { if (validTypes.indexOf(schema.type) === -1) { throw ono.syntax( `Validation failed. ${schemaId} has an invalid type (${schema.type})`); } if (schema.type === "array" && !schema.items) { throw ono.syntax(`Validation failed. ${schemaId} is an array, so it must include an "items" schema`); } } /** * Validates that the declared properties of the given Swagger schema object actually exist. * * @param {object} schema - A Schema object, from the Swagger API * @param {string} schemaId - A value that uniquely identifies the schema object */ function validateRequiredPropertiesExist (schema, schemaId) { /** * Recursively collects all properties of the schema and its ancestors. They are added to the props object. */ function collectProperties (schemaObj, props) { if (schemaObj.properties) { for (let property in schemaObj.properties) { if (schemaObj.properties.hasOwnProperty(property)) { props[property] = schemaObj.properties[property]; } } } if (schemaObj.allOf) { for (let parent of schemaObj.allOf) { collectProperties(parent, props); } } } if (schema.required && Array.isArray(schema.required)) { let props = {}; collectProperties(schema, props); for (let requiredProperty of schema.required) { if (!props[requiredProperty]) { throw ono.syntax( `Validation failed. Property '${requiredProperty}' listed as required but does not exist in '${schemaId}'` ); } } } }