json-machete
Version:
266 lines (265 loc) โข 10.7 kB
JavaScript
import JsonPointer from 'json-pointer';
import urlJoin from 'url-join';
import { handleUntitledDefinitions } from './healUntitledDefinitions.js';
export const resolvePath = (path, root) => {
try {
return JsonPointer.get(root, path);
}
catch (e) {
if (e.message?.startsWith('Invalid reference')) {
return undefined;
}
throw e;
}
};
function isUrl(str) {
return /^https?:\/\//.test(str);
}
function isRefObject(obj) {
return typeof obj === 'object' && typeof obj.$ref === 'string';
}
const getAbsolute$Ref = (given$ref, baseFilePath) => {
const [givenExternalFileRelativePath, givenRefPath] = given$ref.split('#');
if (givenExternalFileRelativePath) {
const cwd = getCwd(baseFilePath);
const givenExternalFilePath = getAbsolutePath(givenExternalFileRelativePath, cwd);
if (givenRefPath) {
return `${givenExternalFilePath}#${givenRefPath}`;
}
return givenExternalFilePath;
}
return `${baseFilePath}#${givenRefPath}`;
};
function normalizeUrl(url) {
return new URL(url).toString();
}
export function getAbsolutePath(path, cwd) {
if (isUrl(path)) {
return path;
}
if (isUrl(cwd)) {
return normalizeUrl(urlJoin(cwd, path));
}
if (path.startsWith('/') || path.substring(1).startsWith(':\\')) {
return path;
}
return cwd + '/' + path;
}
export function getCwd(path) {
const pathParts = path.split('/');
pathParts.pop();
return pathParts.join('/');
}
/**
* Pre-scans a schema document to index all $id and $anchor declarations.
* Per JSON Schema 2020-12 ยง8.2.1, $id applies to the entire lexical scope
* of a schema resource, so all identifiers must be collected before any
* $ref resolution begins.
*/
function buildSchemaIndex(root) {
const ids = new Map();
const anchors = new Map();
const visited = new WeakSet();
function walk(obj, baseId) {
if (obj == null || typeof obj !== 'object' || visited.has(obj))
return;
visited.add(obj);
let currentBaseId = baseId;
if (typeof obj.$id === 'string') {
currentBaseId = obj.$id;
ids.set(currentBaseId, obj);
}
if (typeof obj.$anchor === 'string' && currentBaseId != null) {
anchors.set(`${currentBaseId}#${obj.$anchor}`, obj);
}
for (const key in obj) {
const val = obj[key];
if (typeof val === 'object' && val !== null) {
walk(val, currentBaseId);
}
}
}
walk(root, null);
return { ids, anchors };
}
/**
* Attempts to resolve a split $ref (id part + fragment) against the schema index.
* Returns the target schema object, or undefined to fall through to standard resolution.
*/
function resolveFromSchemaIndex(index, idPart, fragment, root) {
if (idPart) {
const schema = index.ids.get(idPart);
if (!schema)
return undefined;
if (!fragment)
return schema;
if (fragment.startsWith('/'))
return resolvePath(fragment, schema);
return index.anchors.get(`${idPart}#${fragment}`);
}
if (fragment && !fragment.startsWith('/')) {
const rootId = root?.$id;
if (rootId)
return index.anchors.get(`${rootId}#${fragment}`);
}
return undefined;
}
/**
* Walks an object graph and resolves every $ref in-place on its parent.
* Tries three strategies in order: the schema index ($id/$anchor),
* external file/URL loading, and same-document JSON Pointer.
*/
export async function dereferenceObject(obj, { cwd = globalThis?.process.cwd(), externalFileCache = new Map(), refMap = new Map(), root = obj, debugLogFn, readFileOrUrl, resolvedObjects = new WeakSet(), schemaIndex: schemaIndexOpt, }) {
const schemaIndex = schemaIndexOpt ?? buildSchemaIndex(root);
if (obj != null && typeof obj === 'object') {
if (isRefObject(obj)) {
const $ref = obj.$ref;
if (refMap.has($ref)) {
return refMap.get($ref);
}
else {
debugLogFn?.(`Resolving ${$ref}`);
const [externalRelativeFilePath, refPath] = $ref.split('#');
const indexedSchema = resolveFromSchemaIndex(schemaIndex, externalRelativeFilePath, refPath, root);
if (indexedSchema) {
if (resolvedObjects.has(indexedSchema)) {
refMap.set($ref, indexedSchema);
return indexedSchema;
}
const result = await dereferenceObject(indexedSchema, {
cwd,
externalFileCache,
refMap,
root,
debugLogFn,
readFileOrUrl,
resolvedObjects,
schemaIndex,
});
if (!result) {
return obj;
}
resolvedObjects.add(result);
refMap.set($ref, result);
if (!result.$resolvedRef && refPath) {
result.$resolvedRef = refPath;
}
return result;
}
if (externalRelativeFilePath) {
const externalFilePath = getAbsolutePath(externalRelativeFilePath, cwd);
const newCwd = getCwd(externalFilePath);
let externalFile = externalFileCache.get(externalFilePath);
if (!externalFile) {
try {
externalFile = await readFileOrUrl(externalFilePath, { cwd });
}
catch (e) {
console.error(e);
throw new Error(`Unable to load ${externalRelativeFilePath} from ${cwd}`);
}
externalFileCache.set(externalFilePath, externalFile);
// Title should not be overwritten by the title given from the reference
// Usually Swagger and OpenAPI Schemas have this
handleUntitledDefinitions(externalFile);
}
const result = await dereferenceObject(refPath
? {
$ref: `#${refPath}`,
}
: externalFile, {
cwd: newCwd,
externalFileCache,
refMap: new Proxy(refMap, {
get: (originalRefMap, key) => {
switch (key) {
case 'has':
return (given$ref) => {
const original$Ref = getAbsolute$Ref(given$ref, externalFilePath);
return originalRefMap.has(original$Ref);
};
case 'get':
return (given$ref) => {
const original$Ref = getAbsolute$Ref(given$ref, externalFilePath);
return originalRefMap.get(original$Ref);
};
case 'set':
return (given$ref, val) => {
const original$Ref = getAbsolute$Ref(given$ref, externalFilePath);
return originalRefMap.set(original$Ref, val);
};
}
throw new Error('Not implemented ' + key.toString());
},
}),
debugLogFn,
readFileOrUrl,
root: externalFile,
resolvedObjects,
});
refMap.set($ref, result);
resolvedObjects.add(result);
if (result && !result.$resolvedRef) {
result.$resolvedRef = refPath;
}
if (obj.title && !result.title) {
result.title = obj.title;
}
return result;
}
else {
const resolvedObj = resolvePath(refPath, root);
if (resolvedObjects.has(resolvedObj)) {
refMap.set($ref, resolvedObj);
return resolvedObj;
}
/*
if (resolvedObj && !resolvedObj.$resolvedRef) {
resolvedObj.$resolvedRef = refPath;
}
*/
const result = await dereferenceObject(resolvedObj, {
cwd,
externalFileCache,
refMap,
root,
debugLogFn,
readFileOrUrl,
resolvedObjects,
schemaIndex,
});
if (!result) {
return obj;
}
resolvedObjects.add(result);
refMap.set($ref, result);
if (!result.$resolvedRef) {
result.$resolvedRef = refPath;
}
return result;
}
}
}
else {
if (!resolvedObjects.has(obj)) {
resolvedObjects.add(obj);
for (const key in obj) {
const val = obj[key];
if (typeof val === 'object') {
obj[key] = await dereferenceObject(val, {
cwd,
externalFileCache,
refMap,
root,
debugLogFn,
readFileOrUrl,
resolvedObjects,
schemaIndex,
});
}
}
}
}
}
return obj;
}