@redocly/openapi-core
Version:
See https://github.com/Redocly/redocly-cli
215 lines • 9.57 kB
JavaScript
import { asserts, runOnKeysSet, runOnValuesSet } from './asserts.js';
import { colorize } from '../../../logger.js';
import { isRef, isAbsoluteUrl } from '../../../ref-utils.js';
import { isTruthy } from '../../../utils/is-truthy.js';
import { keysOf } from '../../../utils/keys-of.js';
import { isString } from '../../../utils/is-string.js';
import { regexFromString } from '../../../utils/regex-from-string.js';
const assertionMessageTemplates = {
problems: '{{problems}}',
assertionName: '{{assertionName}}',
nodeType: '{{nodeType}}',
key: '{{key}}',
property: '{{property}}',
file: '{{file}}',
pointer: '{{pointer}}',
};
function getPredicatesFromLocators(locators) {
const { filterInParentKeys, filterOutParentKeys, matchParentKeys } = locators;
const keyMatcher = matchParentKeys && regexFromString(matchParentKeys);
const matchKeysPredicate = keyMatcher && ((key) => keyMatcher.test(key.toString()));
const filterInPredicate = Array.isArray(filterInParentKeys) &&
((key) => filterInParentKeys.includes(key.toString()));
const filterOutPredicate = Array.isArray(filterOutParentKeys) &&
((key) => !filterOutParentKeys.includes(key.toString()));
return [matchKeysPredicate, filterInPredicate, filterOutPredicate].filter(isTruthy);
}
export function getAssertsToApply(assertion) {
const assertsToApply = keysOf(asserts)
.filter((assertName) => assertion.assertions[assertName] !== undefined)
.map((assertName) => {
return {
name: assertName,
conditions: assertion.assertions[assertName],
runsOnKeys: runOnKeysSet.has(assertName),
runsOnValues: runOnValuesSet.has(assertName),
};
});
const shouldRunOnKeys = assertsToApply.find((assert) => assert.runsOnKeys && !assert.runsOnValues);
const shouldRunOnValues = assertsToApply.find((assert) => assert.runsOnValues && !assert.runsOnKeys);
if (shouldRunOnValues && !assertion.subject.property) {
throw new Error(`The '${shouldRunOnValues.name}' assertion can't be used on all keys. Please provide a single property.`);
}
if (shouldRunOnKeys && assertion.subject.property) {
throw new Error(`The '${shouldRunOnKeys.name}' assertion can't be used on properties. Please remove the 'property' key.`);
}
return assertsToApply;
}
function getAssertionProperties({ subject }) {
return (Array.isArray(subject.property) ? subject.property : [subject?.property]).filter(Boolean);
}
function applyAssertions(assertionDefinition, asserts, ctx) {
const properties = getAssertionProperties(assertionDefinition);
const assertResults = [];
for (const assert of asserts) {
if (properties.length) {
for (const property of properties) {
assertResults.push(runAssertion({
assert,
ctx,
assertionProperty: property,
}));
}
}
else {
assertResults.push(runAssertion({
assert,
ctx,
}));
}
}
return assertResults.flat();
}
export function buildVisitorObject(assertion, subjectVisitor) {
const targetVisitorLocatorPredicates = getPredicatesFromLocators(assertion.subject);
const targetVisitorSkipFunction = targetVisitorLocatorPredicates.length
? (_, key) => !targetVisitorLocatorPredicates.every((predicate) => predicate(key))
: undefined;
const targetVisitor = {
[assertion.subject.type]: {
enter: subjectVisitor,
...(targetVisitorSkipFunction && { skip: targetVisitorSkipFunction }),
},
};
if (!Array.isArray(assertion.where)) {
return targetVisitor;
}
let currentVisitorLevel = {};
const visitor = currentVisitorLevel;
const context = assertion.where;
for (let index = 0; index < context.length; index++) {
const assertionDefinitionNode = context[index];
if (!isString(assertionDefinitionNode.subject?.type)) {
throw new Error(`${assertion.assertionId} -> where -> [${index}]: 'type' (String) is required`);
}
const locatorPredicates = getPredicatesFromLocators(assertionDefinitionNode.subject);
const assertsToApply = getAssertsToApply(assertionDefinitionNode);
const skipFunction = (node, key, ctx) => !locatorPredicates.every((predicate) => predicate(key)) ||
!!applyAssertions(assertionDefinitionNode, assertsToApply, { ...ctx, node }).length;
const nodeVisitor = {
...((locatorPredicates.length || assertsToApply.length) && { skip: skipFunction }),
};
if (assertionDefinitionNode.subject.type === assertion.subject.type &&
index === context.length - 1) {
// We have to merge the visitors if the last node inside the `where` is the same as the subject.
targetVisitor[assertion.subject.type] = {
enter: subjectVisitor,
...((nodeVisitor.skip && { skip: nodeVisitor.skip }) ||
(targetVisitorSkipFunction && {
skip: (node, key, ctx // We may have locators defined on assertion level and on where level for the same node type
) => !!(nodeVisitor.skip?.(node, key, ctx) || targetVisitorSkipFunction?.(node, key)),
})),
};
}
else {
currentVisitorLevel = currentVisitorLevel[assertionDefinitionNode.subject?.type] =
nodeVisitor;
}
}
currentVisitorLevel[assertion.subject.type] = targetVisitor[assertion.subject.type];
return visitor;
}
export function buildSubjectVisitor(assertId, assertion) {
return (node, ctx) => {
const properties = getAssertionProperties(assertion);
const defaultMessage = `${colorize.blue(assertId)} failed because the ${colorize.blue(assertion.subject.type)} ${colorize.blue(properties.join(', '))} didn't meet the assertions: ${assertionMessageTemplates.problems}`.replace(/ +/g, ' ');
const problems = applyAssertions(assertion, getAssertsToApply(assertion), {
...ctx,
node,
});
if (problems.length) {
for (const problemGroup of groupProblemsByPointer(problems)) {
const message = assertion.message || defaultMessage;
const problemMessage = getProblemsMessage(problemGroup);
const placeholders = {
problems: problemMessage,
assertionName: assertId,
nodeType: assertion.subject.type,
property: properties.join(', '),
key: String(ctx.key),
pointer: ctx.location.pointer,
file: getFilenameFromPath(ctx.location.source.absoluteRef),
};
ctx.report({
message: interpolateMessagePlaceholders(message, placeholders),
location: getProblemsLocation(problemGroup) || ctx.location,
forceSeverity: assertion.severity || 'error',
suggest: assertion.suggest || [],
ruleId: assertId,
});
}
}
};
}
function groupProblemsByPointer(problems) {
const groups = {};
for (const problem of problems) {
if (!problem.location)
continue;
const pointer = problem.location.pointer;
groups[pointer] = groups[pointer] || [];
groups[pointer].push(problem);
}
return Object.values(groups);
}
function getProblemsLocation(problems) {
return problems.length ? problems[0].location : undefined;
}
function getProblemsMessage(problems) {
return problems.length === 1
? problems[0].message ?? ''
: problems.map((problem) => `\n- ${problem.message ?? ''}`).join('');
}
function getFilenameFromPath(absoluteRef) {
if (isAbsoluteUrl(absoluteRef)) {
const parts = absoluteRef.split('/');
return parts.at(-1) || absoluteRef;
}
const parts = absoluteRef.split(/[/\\]/);
return parts.at(-1) || absoluteRef;
}
function interpolateMessagePlaceholders(message, placeholders) {
let result = message;
for (const key of Object.keys(assertionMessageTemplates)) {
const template = assertionMessageTemplates[key];
const value = placeholders[key];
if (!template)
continue;
result = result.split(template).join(value);
}
return result;
}
export function runAssertion({ assert, ctx, assertionProperty, }) {
const currentLocation = assert.name === 'ref' ? ctx.rawLocation : ctx.location;
if (assertionProperty) {
const values = isRef(ctx.node[assertionProperty])
? ctx.resolve(ctx.node[assertionProperty])?.node
: ctx.node[assertionProperty];
const rawValues = ctx.rawNode[assertionProperty];
const location = currentLocation.child(assertionProperty);
return asserts[assert.name](values, assert.conditions, {
...ctx,
baseLocation: location,
rawValue: rawValues,
});
}
else {
const value = Array.isArray(ctx.node) ? ctx.node : Object.keys(ctx.node);
return asserts[assert.name](value, assert.conditions, {
...ctx,
rawValue: ctx.rawNode,
baseLocation: currentLocation,
});
}
}
//# sourceMappingURL=utils.js.map