@ibm-cloud/openapi-ruleset
Version:
Spectral ruleset for validating IBM Cloud services
176 lines (161 loc) • 4.68 kB
JavaScript
/**
* Copyright 2017 - 2023 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const { validateSubschemas } = require('@ibm-cloud/openapi-ruleset-utilities');
const { LoggerFactory } = require('../utils');
// Valid schema types.
const validTypes = [
'array',
'boolean',
'integer',
'number',
'object',
'string',
];
// Valid format values for selected schema types.
const validIntegerFormats = ['int32', 'int64'];
const validNumberFormats = ['float', 'double'];
const validStringFormats = [
'binary',
'byte',
'crn',
'date',
'date-time',
'email',
'identifier',
'password',
'url',
'uuid',
];
// Pre-define some error messages that we can re-use later.
const formatButNoTypeErrorMsg = 'Format defined without a type';
const formatWithMultipleTypesErrorMsg =
'Format defined with multiple types is ambiguous';
const invalidTypeErrorMsg = `Invalid type; valid types are: ${validTypes.join(
', '
)}`;
const integerFormatErrorMsg = `Schema of type integer should use one of the following formats: ${validIntegerFormats.join(
', '
)}`;
const numberFormatErrorMsg = `Schema of type number should use one of the following formats: ${validNumberFormats.join(
', '
)}`;
const stringFormatErrorMsg = `Schema of type string should use one of the following formats: ${validStringFormats.join(
', '
)}`;
let ruleId;
let logger;
module.exports = function (schema, _opts, context) {
if (!logger) {
ruleId = context.rule.name;
logger = LoggerFactory.getInstance().getLogger(ruleId);
}
return validateSubschemas(schema, context.path, typeFormatErrors);
};
function typeFormatErrors(schema, path) {
logger.debug(
`${ruleId}: checking schema [type=${schema.type}, format=${
schema.format
}] at location: ${path.join('.')}`
);
// Determine the schema's type. This could be either
// a string or a list of strings. If "type" is a list,
// we'll expect it to contain only a single non-null type
// if "format" is also specified.
const format = schema.format ? schema.format.trim().toLowerCase() : undefined;
let type;
if (typeof schema.type === 'string') {
type = schema.type.trim().toLowerCase();
} else if (Array.isArray(schema.type)) {
// Not interested in any 'null' type values.
const filteredTypes = schema.type.filter(t => t !== 'null');
if (filteredTypes.length === 1) {
// If we have a single entry in type array, let's use it.
type = filteredTypes[0].trim().toLowerCase();
} else if (filteredTypes.length > 1) {
// If multiple types are defined along with format, that's an error.
if (format) {
logger.debug(
`${ruleId}: error: found multiple types along with format.\n`
);
return [
{
message: formatWithMultipleTypesErrorMsg,
path,
},
];
}
}
}
// If "type" isn't specified but format IS specified, that's an error.
if (!type) {
if (format) {
logger.debug(
`${ruleId}: error: type is omitted but format was specified.\n`
);
return [
{
message: formatButNoTypeErrorMsg,
path,
},
];
}
return [];
}
const errors = [];
// Type is defined, so let's first make sure that it is a valid type.
if (!validTypes.includes(type)) {
errors.push({
message: invalidTypeErrorMsg,
path,
});
} else {
// Type is valid, let's make sure format is valid for this type.
switch (type) {
case 'integer':
if (format && !validIntegerFormats.includes(format)) {
errors.push({
message: integerFormatErrorMsg,
path,
});
}
break;
case 'number':
if (format && !validNumberFormats.includes(format)) {
errors.push({
message: numberFormatErrorMsg,
path,
});
}
break;
case 'string':
if (format && !validStringFormats.includes(format)) {
errors.push({
message: stringFormatErrorMsg,
path,
});
}
break;
case 'boolean':
case 'object':
case 'array':
// No valid formats for boolean, format should be undefined
if (format !== undefined) {
errors.push({
message: `Schema of type ${type} should not have a format`,
path,
});
}
break;
}
}
if (errors.length) {
logger.debug(
`${ruleId}: found these errors:\n${JSON.stringify(errors, null, 2)}`
);
} else {
logger.debug(`${ruleId}: PASSED!`);
}
return errors;
}