UNPKG

json-machete

Version:
274 lines (273 loc) โ€ข 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.resolvePath = void 0; exports.getAbsolutePath = getAbsolutePath; exports.getCwd = getCwd; exports.dereferenceObject = dereferenceObject; const tslib_1 = require("tslib"); const json_pointer_1 = tslib_1.__importDefault(require("json-pointer")); const url_join_1 = tslib_1.__importDefault(require("url-join")); const healUntitledDefinitions_js_1 = require("./healUntitledDefinitions.js"); const resolvePath = (path, root) => { try { return json_pointer_1.default.get(root, path); } catch (e) { if (e.message?.startsWith('Invalid reference')) { return undefined; } throw e; } }; exports.resolvePath = resolvePath; 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(); } function getAbsolutePath(path, cwd) { if (isUrl(path)) { return path; } if (isUrl(cwd)) { return normalizeUrl((0, url_join_1.default)(cwd, path)); } if (path.startsWith('/') || path.substring(1).startsWith(':\\')) { return path; } return cwd + '/' + path; } 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 (0, exports.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. */ 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 (0, healUntitledDefinitions_js_1.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 = (0, exports.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; }