@ibm-cloud/openapi-ruleset
Version:
Spectral ruleset for validating IBM Cloud services
153 lines (128 loc) • 4.13 kB
JavaScript
/**
* Copyright 2024 - 2025 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const {
isObject,
isObjectSchema,
schemaHasConstraint,
validateNestedSchemas,
} = require('@ibm-cloud/openapi-ruleset-utilities');
const { LoggerFactory } = require('../utils');
let ruleId;
let logger;
/**
* The implementation for this rule makes assumptions that are dependent on the
* presence of the following other rules:
*
* - ibm-pattern-properties: patternProperties isn't empty or the wrong type
*/
module.exports = function (schema, _opts, context) {
if (!logger) {
ruleId = context.rule.name;
logger = LoggerFactory.getInstance().getLogger(ruleId);
}
return validateNestedSchemas(schema, context.path, wellDefinedDictionaries);
};
function wellDefinedDictionaries(schema, path) {
// We only care about object schemas.
if (!isObject(schema) || !isObjectSchema(schema)) {
return [];
}
// We will flag dictionaries of dictionaries, so we can skip
// providing guidance for directly nested dictionaries.
if (isDictionaryValueSchema(path)) {
return [];
}
logger.debug(
`${ruleId}: checking object schema at location: ${path.join('.')}`
);
// Dictionaries should have additionalProperties or patternProperties
// defined on them. If the schema doesn't, make sure it has properties
// and then abandon the check.
if (!isDictionarySchema(schema)) {
if (!schemaDefinesField(schema, 'properties')) {
return [
{
message:
'Object schemas must define either properties, or (additional/pattern)Properties with a concrete type',
path,
},
];
}
logger.debug(
`${ruleId}: object schema at location ${path.join(
'.'
)} is a model, no need to check`
);
return [];
}
const errors = [];
// An object cannot try to be a model and a dictionary at the same time.
// It can't have both properties and additionalProperties.
if (schemaDefinesField(schema, 'properties')) {
errors.push({
message:
'Object schemas must be either a model or a dictionary - they cannot be both',
path,
});
}
// Make sure the dictionary has a defined type. We may want to make this check
// more strict in the future but this meets our current purposes.
if (schemaHasConstraint(schema, isAmbiguousDictionary)) {
errors.push({
message: 'Dictionary schemas must have a single, well-defined value type',
path,
});
}
// The Handbook guidelines state that dictionary types
// should not be dictionaries themselves.
if (schemaHasConstraint(schema, isDictionaryOfDictionaries)) {
errors.push({
message: 'Dictionaries must not have values that are also dictionaries',
path,
});
}
return errors;
}
function schemaDefinesField(schema, field) {
return schemaHasConstraint(schema, s => !!s[field]);
}
function isAmbiguousDictionary(schema) {
return dictionaryValuesHaveConstraint(
schema,
valueSchema =>
!isObject(valueSchema) || !schemaDefinesField(valueSchema, 'type')
);
}
function isDictionaryOfDictionaries(schema) {
return dictionaryValuesHaveConstraint(
schema,
valueSchema => isObject(valueSchema) && isDictionarySchema(valueSchema)
);
}
function dictionaryValuesHaveConstraint(schema, hasConstraint) {
return schemaHasConstraint(schema, s => {
if (s.additionalProperties !== undefined) {
return hasConstraint(s.additionalProperties);
}
if (s.patternProperties !== undefined) {
return Object.values(s.patternProperties).some(p => hasConstraint(p));
}
return false;
});
}
// Check, *by path*, if the current schema is a dictionary value schema.
function isDictionaryValueSchema(path) {
return (
path.at(-1) === 'additionalProperties' ||
path.at(-2) === 'patternProperties'
);
}
// Check, *by object fields* if the current schema is a dictionary or not.
function isDictionarySchema(schema) {
return (
schemaDefinesField(schema, 'additionalProperties') ||
schemaDefinesField(schema, 'patternProperties')
);
}