@redocly/openapi-core
Version:
See https://github.com/Redocly/redocly-cli
326 lines • 13.4 kB
JavaScript
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