@ibm-cloud/openapi-ruleset
Version:
Spectral ruleset for validating IBM Cloud services
115 lines (101 loc) • 3.65 kB
JavaScript
/**
* Copyright 2017 - 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const { LoggerFactory } = require('../utils');
let ruleId;
let logger;
module.exports = function ($ref, _opts, context) {
if (!logger) {
ruleId = context.rule.name;
logger = LoggerFactory.getInstance().getLogger(ruleId);
}
return checkRefPattern($ref, context.path);
};
// This object is used to categorize a $ref value, based on its path (location within the API document).
// Each entry corresponds to a particular type of object to which we support references,
// and coincidentally they also correspond to the various sections within the "components" field of the API definition.
// If a $ref property's path matches against an entry's refPathRegex value, then that entry is used to
// validate the $ref value (i.e. the $ref value must start with that entry's prefix value).
// The entries of this object are ordered in descending order of presumed prevalence.
const validRefPrefixes = {
schemas: {
refPathRegex:
/,((schema)|(properties,[^,]+)|(items)|(additionalProperties)|((allOf|anyOf|oneOf),\d+))$/,
prefix: '#/components/schemas/',
},
parameters: {
refPathRegex: /,parameters,\d+$/,
prefix: '#/components/parameters/',
},
responses: {
refPathRegex: /,responses,[^,]+$/,
prefix: '#/components/responses/',
},
requestBodies: {
refPathRegex: /,requestBody$/,
prefix: '#/components/requestBodies/',
},
links: {
refPathRegex: /,links,[^,]+$/,
prefix: '#/components/links/',
},
examples: {
refPathRegex: /,examples,[^,]+$/,
prefix: '#/components/examples/',
},
headers: {
refPathRegex: /,headers,[^,]+$/,
prefix: '#/components/headers/',
},
securitySchemes: {
refPathRegex: /,securitySchemes,[^,]+$/,
prefix: '#/components/securitySchemes/',
},
callbacks: {
refPathRegex: /,callbacks,[^,]+$/,
prefix: '#/components/callbacks/',
},
};
function checkRefPattern($ref, path) {
// $ref *may* be the name of a property, etc. so this check makes sure we are
// validating a $ref property that points to a string value, which should
// nearly always be an actual reference value.
if (typeof $ref !== 'string') {
return [];
}
// We're interested only in local refs (e.g. #/components/schemas/MyModel).
if (!$ref.startsWith('#')) {
logger.debug(`${ruleId}: skipping check for external $ref: ${$ref}`);
return [];
}
logger.debug(
`${ruleId}: checking $ref '${$ref}' at location: ${path.join('.')}`
);
// Compute the path of the $ref property's parent object
// by just removing the last element of "path" (which will be '$ref').
const parentPath = path.slice(0, path.length - 1);
const pathStr = parentPath.join(',');
// Walk through the entries of "validRefPrefixes" until we find the entry
// that applies to the location where the $ref property was found.
for (const [refType, entry] of Object.entries(validRefPrefixes)) {
if (pathStr.match(entry.refPathRegex)) {
if (!$ref.startsWith(entry.prefix)) {
logger.debug(
`${ruleId}: $ref '${$ref}' (type '${refType}') should start with '${entry.prefix}'`
);
return [
{
message: `$refs to ${refType} should start with '${entry.prefix}'`,
path,
},
];
}
// We want to do the check using only the first (and presumably only) "validRefPrefixes"
// entry that matches the current $ref property value being checked.
// So if the above check succeeds, then just bail out now.
return [];
}
}
return [];
}