UNPKG

@redocly/openapi-core

Version:

See https://github.com/Redocly/redocly-cli

215 lines 9.57 kB
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