You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
344 lines
12 KiB
344 lines
12 KiB
"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}'`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|