UNPKG

@redocly/openapi-core

Version:

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

326 lines 13.4 kB
import * as fs from 'node:fs'; import * as path from 'node:path'; import { isRef, joinPointer, escapePointer, parseRef, isAbsoluteUrl, isAnchor, isExternalValue, } from './ref-utils.js'; import { isNamedType, SpecExtension } from './types/index.js'; import { getOwn } from './utils/get-own.js'; import { readFileFromUrl } from './utils/read-file-from-url.js'; import { parseYaml } from './js-yaml/index.js'; import { nextTick } from './utils/next-tick.js'; import { YamlParseError } from './errors/yaml-parse-error.js'; import { makeRefId } from './utils/make-ref-id.js'; export class Source { constructor(absoluteRef, body, mimeType) { this.absoluteRef = absoluteRef; this.body = body; this.mimeType = mimeType; } // pass safeLoad as argument to separate it from browser bundle getAst(safeLoad) { if (this._ast === undefined) { this._ast = safeLoad(this.body, { filename: this.absoluteRef }) ?? undefined; // fix ast representation of file with newlines only if (this._ast && this._ast.kind === 0 && // KIND.scalar = 0 this._ast.value === '' && this._ast.startPosition !== 1) { this._ast.startPosition = 1; this._ast.endPosition = 1; } } return this._ast; } getLines() { if (this._lines === undefined) { this._lines = this.body.split(/\r\n|[\n\r]/g); } return this._lines; } } export class ResolveError extends Error { constructor(originalError) { super(originalError.message); this.originalError = originalError; // Set the prototype explicitly. Object.setPrototypeOf(this, ResolveError.prototype); } } export function makeDocumentFromString(sourceString, absoluteRef) { const source = new Source(absoluteRef, sourceString); try { return { source, parsed: parseYaml(sourceString, { filename: absoluteRef }), }; } catch (e) { throw new YamlParseError(e, source); } } export class BaseResolver { constructor(config = { http: { headers: [] } }) { this.config = config; this.cache = new Map(); } getFiles() { return new Set(Array.from(this.cache.keys())); } resolveExternalRef(base, ref) { if (isAbsoluteUrl(ref)) { return ref; } if (base && isAbsoluteUrl(base)) { return new URL(ref, base).href; } return path.resolve(base ? path.dirname(base) : process.cwd(), ref); } async loadExternalRef(absoluteRef) { try { if (isAbsoluteUrl(absoluteRef)) { const { body, mimeType } = await readFileFromUrl(absoluteRef, this.config.http); return new Source(absoluteRef, body, mimeType); } else { if (fs.lstatSync(absoluteRef).isDirectory()) { throw new Error(`Expected a file but received a folder at ${absoluteRef}.`); } const content = await fs.promises.readFile(absoluteRef, 'utf-8'); // In some cases file have \r\n line delimeters like on windows, we should skip it. return new Source(absoluteRef, content.replace(/\r\n/g, '\n')); } } catch (error) { error.message = error.message.replace(', lstat', ''); throw new ResolveError(error); } } parseDocument(source, isRoot = false) { const ext = source.absoluteRef.substr(source.absoluteRef.lastIndexOf('.')); if (!['.json', '.json', '.yml', '.yaml'].includes(ext) && !source.mimeType?.match(/(json|yaml|openapi)/) && !isRoot // always parse root ) { return { source, parsed: source.body }; } try { return { source, parsed: parseYaml(source.body, { filename: source.absoluteRef }), }; } catch (e) { throw new YamlParseError(e, source); } } async resolveDocument(base, ref, isRoot = false) { const absoluteRef = this.resolveExternalRef(base, ref); const cachedDocument = this.cache.get(absoluteRef); if (cachedDocument) { return cachedDocument; } const doc = this.loadExternalRef(absoluteRef).then((source) => { return this.parseDocument(source, isRoot); }); this.cache.set(absoluteRef, doc); return doc; } } function pushRef(head, node) { return { prev: head, node, }; } function hasRef(head, node) { while (head) { if (head.node === node) { return true; } head = head.prev; } return false; } const unknownType = { name: 'unknown', properties: {} }; const resolvableScalarType = { name: 'scalar', properties: {} }; export async function resolveDocument(opts) { const { rootDocument, externalRefResolver, rootType } = opts; const resolvedRefMap = new Map(); const seenNodes = new Set(); // format "${type}::${absoluteRef}${pointer}" const resolvePromises = []; resolveRefsInParallel(rootDocument.parsed, rootDocument, '#/', rootType); let resolved; do { resolved = await Promise.all(resolvePromises); } while (resolvePromises.length !== resolved.length); return resolvedRefMap; function resolveRefsInParallel(rootNode, rootNodeDocument, rootNodePointer, type) { const rootNodeDocAbsoluteRef = rootNodeDocument.source.absoluteRef; const anchorRefsMap = new Map(); walk(rootNode, type, rootNodeDocAbsoluteRef + rootNodePointer); function walk(node, type, nodeAbsoluteRef) { if (typeof node !== 'object' || node === null) { return; } const nodeId = `${type.name}::${nodeAbsoluteRef}`; if (seenNodes.has(nodeId)) { return; } seenNodes.add(nodeId); const [_, anchor] = Object.entries(node).find(([key]) => key === '$anchor') || []; if (anchor) { anchorRefsMap.set(`#${anchor}`, node); } if (Array.isArray(node)) { const itemsType = type.items; // we continue resolving unknown types, but stop early on known scalars if (itemsType === undefined && type !== unknownType && type !== SpecExtension) { return; } const isTypeAFunction = typeof itemsType === 'function'; for (let i = 0; i < node.length; i++) { let itemType = isTypeAFunction ? itemsType(node[i], joinPointer(nodeAbsoluteRef, i)) : itemsType; // we continue resolving unknown types, but stop early on known scalars if (itemType === undefined && type !== unknownType && type !== SpecExtension) { continue; } const value = itemType?.directResolveAs ? { $ref: node[i] } : node[i]; itemType = itemType?.directResolveAs || itemType; walk(value, isNamedType(itemType) ? itemType : unknownType, joinPointer(nodeAbsoluteRef, i)); } return; } for (const propName of Object.keys(node)) { let propValue = node[propName]; let propType = getOwn(type.properties, propName); if (propType === undefined) propType = type.additionalProperties; if (typeof propType === 'function') propType = propType(propValue, propName); if (propType === undefined) propType = unknownType; if (type.extensionsPrefix && propName.startsWith(type.extensionsPrefix) && propType === unknownType) { propType = SpecExtension; } if (!isNamedType(propType) && propType?.directResolveAs) { propType = propType.directResolveAs; propValue = { $ref: propValue }; } if (propType && propType.name === undefined && propType.resolvable !== false) { propType = resolvableScalarType; } if (!isNamedType(propType) || typeof propValue !== 'object') { continue; } walk(propValue, propType, joinPointer(nodeAbsoluteRef, escapePointer(propName))); } if (isRef(node)) { const promise = followRef(rootNodeDocument, node, { prev: null, node, }).then((resolvedRef) => { if (resolvedRef.resolved) { resolveRefsInParallel(resolvedRef.node, resolvedRef.document, resolvedRef.nodePointer, type); } }); resolvePromises.push(promise); } // handle example.externalValue as reference if (isExternalValue(node)) { const promise = followRef(rootNodeDocument, { $ref: node.externalValue }, { prev: null, node, }).then((resolvedRef) => { if (resolvedRef.resolved) { resolveRefsInParallel(resolvedRef.node, resolvedRef.document, resolvedRef.nodePointer, type); } }); resolvePromises.push(promise); } } async function followRef(document, ref, refStack) { if (hasRef(refStack.prev, ref)) { throw new Error('Self-referencing circular pointer'); } if (isAnchor(ref.$ref)) { // Wait for all anchors in the document to be collected firstly. await nextTick(); const resolvedRef = { resolved: true, isRemote: false, node: anchorRefsMap.get(ref.$ref), document, nodePointer: ref.$ref, }; const refId = makeRefId(document.source.absoluteRef, ref.$ref); resolvedRefMap.set(refId, resolvedRef); return resolvedRef; } const { uri, pointer } = parseRef(ref.$ref); const isRemote = uri !== null && externalRefResolver.resolveExternalRef(document.source.absoluteRef, uri) !== document.source.absoluteRef; let targetDoc; try { targetDoc = isRemote ? (await externalRefResolver.resolveDocument(document.source.absoluteRef, uri)) : document; } catch (error) { const resolvedRef = { resolved: false, isRemote, document: undefined, error: error, }; const refId = makeRefId(document.source.absoluteRef, ref.$ref); resolvedRefMap.set(refId, resolvedRef); return resolvedRef; } let resolvedRef = { resolved: true, document: targetDoc, isRemote, node: document.parsed, nodePointer: '#/', }; let target = targetDoc.parsed; const segments = pointer; for (const segment of segments) { if (typeof target !== 'object') { target = undefined; break; } else if (target[segment] !== undefined) { target = target[segment]; resolvedRef.nodePointer = joinPointer(resolvedRef.nodePointer, escapePointer(segment)); } else if (isRef(target)) { resolvedRef = await followRef(targetDoc, target, pushRef(refStack, target)); targetDoc = resolvedRef.document || targetDoc; if (typeof resolvedRef.node !== 'object') { target = undefined; break; } target = resolvedRef.node[segment]; resolvedRef.nodePointer = joinPointer(resolvedRef.nodePointer, escapePointer(segment)); } else { target = undefined; break; } } resolvedRef.node = target; resolvedRef.document = targetDoc; const refId = makeRefId(document.source.absoluteRef, ref.$ref); if (resolvedRef.document && isRef(target)) { resolvedRef = await followRef(resolvedRef.document, target, pushRef(refStack, target)); } resolvedRefMap.set(refId, resolvedRef); return { ...resolvedRef }; } } } //# sourceMappingURL=resolve.js.map