UNPKG

@redocly/openapi-core

Version:

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

145 lines 5.72 kB
import { isPlainObject } from '../../utils/is-plain-object.js'; const pathRegex = /\{([a-zA-Z0-9_.-]+)\}+/g; const MAX_DEPTH = 2; // Only first callback level is supported export const PathParamsDefined = () => { const pathContext = { current: null }; const currentOperationParams = new Set(); return { PathItem: { enter(_, { key }) { pathItemEnter(pathContext, key); }, leave() { pathItemLeave(pathContext); }, Parameter(parameter, { report, location, rawLocation }) { createPathItemParameterHandler({ parameter, pathContext, report, location, rawLocation }); }, Operation: createOperationHandlers(pathContext, currentOperationParams), }, }; }; const pathItemEnter = (pathContext, key) => { pathContext.current = { path: key, templateParams: extractTemplateParams(key), definedParams: new Set(), }; }; const pathItemLeave = (pathContext) => { pathContext.current = null; }; const createPathItemParameterHandler = ({ parameter, pathContext, report, location, rawLocation, }) => { if (parameter.in === 'path' && parameter.name && pathContext.current) { pathContext.current.definedParams.add(parameter.name); validatePathParameter({ paramName: parameter.name, templateParams: pathContext.current.templateParams, path: pathContext.current.path, report, location, rawLocation, }); } }; const createOperationHandlers = (pathContext, currentOperationParams, depth = 0) => { const reportMaxDepthWarning = (report, location, depth) => { report({ message: `Maximum callback nesting depth (${depth}) reached. Path parameter validation is limited beyond this depth to prevent infinite recursion.`, location: location, }); }; if (depth >= MAX_DEPTH) { return { enter: () => { }, leave: (_op, { report, location }) => { reportMaxDepthWarning(report, location, depth); }, Parameter: () => { }, Callback: undefined, }; } const createCallbackPathItem = () => { let parentPathContext = null; return { enter(_, { key }) { parentPathContext = pathContext.current; pathItemEnter(pathContext, key); }, leave() { pathContext.current = parentPathContext; }, Parameter(parameter, { report, location, rawLocation }) { createPathItemParameterHandler({ parameter, pathContext, report, location, rawLocation }); }, get Operation() { return createOperationHandlers(pathContext, currentOperationParams, depth + 1); }, }; }; return { enter() { currentOperationParams = new Set(); }, leave(_operation, { report, location }) { if (!pathContext.current || !currentOperationParams) return; validateRequiredPathParams({ templateParams: pathContext.current.templateParams, definedOperationParams: currentOperationParams, definedPathParams: pathContext.current.definedParams, path: pathContext.current.path, report, location, }); }, Parameter(parameter, { report, location, rawLocation }) { collectPathParamsFromOperation(parameter, currentOperationParams); if (parameter.in === 'path' && parameter.name && pathContext.current) { currentOperationParams.add(parameter.name); validatePathParameter({ paramName: parameter.name, templateParams: pathContext.current.templateParams, path: pathContext.current.path, report, location, rawLocation, }); } }, Callback: { get PathItem() { return createCallbackPathItem(); }, }, }; }; const extractTemplateParams = (path) => { return new Set(Array.from(path.matchAll(pathRegex)).map((m) => m[1])); }; const collectPathParamsFromOperation = (parameter, targetSet) => { if (isPlainObject(parameter) && 'in' in parameter && 'name' in parameter) { if (parameter.in === 'path' && parameter.name) { targetSet.add(parameter.name); } } }; const validatePathParameter = ({ paramName, templateParams, path, report, location, rawLocation, }) => { if (!templateParams.has(paramName)) { const message = `Path parameter \`${paramName}\` is not used in the path \`${path}\`.`; const from = rawLocation === location ? undefined : rawLocation; report({ message, location: location.child(['name']), from }); } }; const validateRequiredPathParams = ({ templateParams, definedOperationParams, definedPathParams, path, report, location, }) => { const allDefinedParams = new Set([...definedOperationParams, ...definedPathParams]); for (const templateParam of templateParams) { if (!allDefinedParams.has(templateParam)) { report({ message: `The operation does not define the path parameter \`{${templateParam}}\` expected by path \`${path}\`.`, location: location.child(['parameters']).key(), }); } } }; //# sourceMappingURL=path-params-defined.js.map