@ibm-cloud/openapi-ruleset
Version:
Spectral ruleset for validating IBM Cloud services
329 lines (292 loc) • 8.92 kB
JavaScript
/**
* Copyright 2017 - 2023 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const {
isArraySchema,
isIntegerSchema,
isObject,
isStringSchema,
} = require('@ibm-cloud/openapi-ruleset-utilities');
const { LoggerFactory } = require('../utils');
let ruleId;
let logger;
module.exports = function (schema, _opts, context) {
if (!logger) {
ruleId = context.rule.name;
logger = LoggerFactory.getInstance().getLogger(ruleId);
}
return checkErrorContainerModelSchema(schema, context.path);
};
/**
* This function will verify that "schema" complies with the API Handbook's
* "Errors" section (https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-errors), specifically
* that it complies with the "Error container model" design requirements.
* Here are the specific checks that are performed:
* 1. "schema" contains the "trace" property (a string)
* 2. "schema" contains the "errors" property (an array)
* 3. if "schema" contains the "status_code" property it must be an integer
* 4. the "errors" property's "items" field must be a valid "error model"
* 5. the error model schema (i.e. errors.items) contains the "code" string property
* which must have an enum identifying the valid error codes that could be returned by
* the server.
* 6. the error model schema contains the "message" property (a string)
* 7. the error model schema contains the "more_info" property (a string)
* 8. if the error model schema contains the "target" property, it must be a valid
* error target model with string properties "type" and "name" present
* @param {*} schema the schema to be verified as an "Error container model" schema
* @param {*} path the array of path segments indicating the "location" of the schema within the API definition
* (e.g. ['paths','/v1/clouds/{id}', 'get', 'responses', '400', 'content', 'application/json', 'schema'])
* @returns an array containing the violations found or [] if no violations
*/
function checkErrorContainerModelSchema(schema, path) {
logger.debug(
`${ruleId}: checking error container model at location: ${path.join('.')}`
);
if (!schemaIsObjectWithProperties(schema)) {
logger.debug(`${ruleId}: it's not an object with properties!`);
return [
{
message: 'Error container model must be an object with properties',
path,
},
];
}
const properties = schema.properties;
path = [...path, 'properties'];
const errors = [];
errors.push(...checkTraceProperty(properties, path));
errors.push(...checkStatusCodeProperty(properties, path));
errors.push(...checkErrorsProperty(properties, path));
if (errors.length) {
logger.debug(
`${ruleId}: found one or more violations:\n${JSON.stringify(errors)}`
);
} else {
logger.debug(`${ruleId}: PASSED!`);
}
return errors;
}
function checkTraceProperty(properties, path) {
const prop = properties.trace;
if (prop) {
path = [...path, 'trace'];
}
if (!isObject(prop) || !isStringSchema(prop)) {
return [
{
message: `Error container model should contain property 'trace' of type string`,
path,
},
];
}
return [];
}
function checkStatusCodeProperty(properties, path) {
// It's ok to omit the status_code property. But if present, it MUST be an integer.
const prop = properties.status_code;
if (!prop) {
return [];
}
if (!isObject(prop) || !isIntegerSchema(prop)) {
return [
{
message: `Error container model property 'status_code' must be of type integer`,
path: [...path, 'status_code'],
},
];
}
return [];
}
function checkErrorsProperty(properties, path) {
path = [...path];
const prop = properties.errors;
if (prop) {
path.push('errors');
}
if (!isObject(prop) || !isArraySchema(prop)) {
return [
{
message: `Error container model must contain property 'errors' which must be an array of error models`,
path,
},
];
}
const items = prop.items;
if (items) {
path.push('items');
}
if (!schemaIsObjectWithProperties(items)) {
return [
{
message: `Error container model 'errors.items' field must be an object with properties`,
path,
},
];
}
return validateErrorModelSchema(items, path);
}
function validateErrorModelSchema(schema, path) {
path = [...path, 'properties'];
const properties = schema.properties;
const errors = [];
errors.push(...checkCodeProperty(properties, path));
errors.push(...checkMessageProperty(properties, path));
errors.push(...checkMoreInfoProperty(properties, path));
errors.push(...checkTargetProperty(properties, path));
return errors;
}
function checkCodeProperty(properties, path) {
path = [...path];
const prop = properties.code;
if (!prop) {
return [
{
message: `Error model must contain property 'code' of type string`,
path,
},
];
}
path.push('code');
if (!isStringSchema(prop)) {
return [
{
message: `Error model must contain property 'code' of type string`,
path,
},
];
}
if (!prop.enum) {
return [
{
message: `Error model property 'code' must include an enumeration of the valid error codes`,
path,
},
];
}
path.push('enum');
if (!Array.isArray(prop.enum)) {
return [
{
message: `Error model property 'code' must include an enumeration of the valid error codes`,
path,
},
];
}
return [];
}
function checkMessageProperty(properties, path) {
const prop = properties.message;
if (prop) {
path = [...path, 'message'];
}
if (!isObject(prop) || !isStringSchema(prop)) {
return [
{
message: `Error model must contain property 'message' of type string`,
path,
},
];
}
return [];
}
function checkMoreInfoProperty(properties, path) {
const prop = properties.more_info;
if (prop) {
path = [...path, 'more_info'];
}
if (!isObject(prop) || !isStringSchema(prop)) {
return [
{
message: `Error model should contain property 'more_info' of type string`,
path,
},
];
}
return [];
}
function checkTargetProperty(properties, path) {
// It's ok to omit the target property.
const target = properties.target;
if (!target) {
return [];
}
path = [...path, 'target'];
// If target is defined, it must comply with the API Handbook.
if (!schemaIsObjectWithProperties(target)) {
return [
{
message: `Error model property 'target' must be a valid error target model object`,
path,
},
];
}
const targetModelProps = target.properties;
path.push('properties');
const errors = [];
// 1. The 'type' property must be defined as an enumeration: ['field', 'header', 'parameter'].
const typeProp = targetModelProps.type;
if (!typeProp) {
errors.push({
message: `Error target model must contain property 'type' of type string`,
path,
});
} else if (!isStringSchema(typeProp)) {
errors.push({
message: `Error target model must contain property 'type' of type string`,
path: [...path, 'type'],
});
} else {
// We know that 'type' is a string property. Now make sure its enum complies.
const enumValue = typeProp.enum;
const message = `Error target model property 'type' must define an enumeration containing ['field', 'header', 'parameter']`;
if (!enumValue) {
errors.push({
message,
path: [...path, 'type'],
});
} else if (!Array.isArray(enumValue)) {
errors.push({
message,
path: [...path, 'type', 'enum'],
});
} else {
const validEnum = ['field', 'parameter', 'header'];
if (!listsEqual(validEnum, enumValue)) {
errors.push({
message,
path: [...path, 'type', 'enum'],
});
}
}
}
// 2. The 'name' property must be of type string.
const nameProp = targetModelProps.name;
if (!nameProp) {
errors.push({
message: `Error target model must contain property 'name' of type string`,
path,
});
} else if (!isStringSchema(nameProp)) {
errors.push({
message: `Error target model must contain property 'name' of type string`,
path: [...path, 'name'],
});
}
return errors;
}
/**
* Returns true iff "list1" equals "list2" without regard to the order of elements.
* @param {*} list1 the first list to compare
* @param {*} list2 the second list to compare
* @returns boolean true iff the lists contain the same elements even if in a different order
*/
function listsEqual(list1, list2) {
if (list1.length != list2.length) {
return false;
}
return list1.every(x => list2.includes(x));
}
function schemaIsObjectWithProperties(s) {
return s && isObject(s) && isObject(s.properties);
}