[d24f17c] | 1 | import jsYaml from 'js-yaml';
|
---|
| 2 | import { url } from '@swagger-api/apidom-reference/configuration/empty';
|
---|
| 3 | import '../../helpers/fetch-polyfill.node.js';
|
---|
| 4 | import lib from './index.js';
|
---|
| 5 | import createError from './create-error.js';
|
---|
| 6 | import { isFreelyNamed, absolutifyPointer } from '../helpers.js';
|
---|
| 7 | import { ACCEPT_HEADER_VALUE_FOR_DOCUMENTS } from '../../constants.js';
|
---|
| 8 | const ABSOLUTE_URL_REGEXP = /^([a-z]+:\/\/|\/\/)/i;
|
---|
| 9 | const JSONRefError = createError('JSONRefError', function cb(message, extra, oriError) {
|
---|
| 10 | this.originalError = oriError;
|
---|
| 11 | Object.assign(this, extra || {});
|
---|
| 12 | });
|
---|
| 13 | const docCache = {};
|
---|
| 14 | const specmapRefs = new WeakMap();
|
---|
| 15 | const skipResolutionTestFns = [
|
---|
| 16 | // OpenAPI 2.0 response examples
|
---|
| 17 | path =>
|
---|
| 18 | // ["paths", *, *, "responses", *, "examples"]
|
---|
| 19 | path[0] === 'paths' && path[3] === 'responses' && path[5] === 'examples',
|
---|
| 20 | // OpenAPI 3.0 Response Media Type Examples
|
---|
| 21 | path =>
|
---|
| 22 | // ["paths", *, *, "responses", *, "content", *, "example"]
|
---|
| 23 | path[0] === 'paths' && path[3] === 'responses' && path[5] === 'content' && path[7] === 'example', path =>
|
---|
| 24 | // ["paths", *, *, "responses", *, "content", *, "examples", *, "value"]
|
---|
| 25 | path[0] === 'paths' && path[3] === 'responses' && path[5] === 'content' && path[7] === 'examples' && path[9] === 'value',
|
---|
| 26 | // OpenAPI 3.0 Request Body Media Type Examples
|
---|
| 27 | path =>
|
---|
| 28 | // ["paths", *, *, "requestBody", "content", *, "example"]
|
---|
| 29 | path[0] === 'paths' && path[3] === 'requestBody' && path[4] === 'content' && path[6] === 'example', path =>
|
---|
| 30 | // ["paths", *, *, "requestBody", "content", *, "examples", *, "value"]
|
---|
| 31 | path[0] === 'paths' && path[3] === 'requestBody' && path[4] === 'content' && path[6] === 'examples' && path[8] === 'value',
|
---|
| 32 | // OAS 3.0 Parameter Examples
|
---|
| 33 | path =>
|
---|
| 34 | // ["paths", *, "parameters", *, "example"]
|
---|
| 35 | path[0] === 'paths' && path[2] === 'parameters' && path[4] === 'example', path =>
|
---|
| 36 | // ["paths", *, *, "parameters", *, "example"]
|
---|
| 37 | path[0] === 'paths' && path[3] === 'parameters' && path[5] === 'example', path =>
|
---|
| 38 | // ["paths", *, "parameters", *, "examples", *, "value"]
|
---|
| 39 | path[0] === 'paths' && path[2] === 'parameters' && path[4] === 'examples' && path[6] === 'value', path =>
|
---|
| 40 | // ["paths", *, *, "parameters", *, "examples", *, "value"]
|
---|
| 41 | path[0] === 'paths' && path[3] === 'parameters' && path[5] === 'examples' && path[7] === 'value', path =>
|
---|
| 42 | // ["paths", *, "parameters", *, "content", *, "example"]
|
---|
| 43 | path[0] === 'paths' && path[2] === 'parameters' && path[4] === 'content' && path[6] === 'example', path =>
|
---|
| 44 | // ["paths", *, "parameters", *, "content", *, "examples", *, "value"]
|
---|
| 45 | path[0] === 'paths' && path[2] === 'parameters' && path[4] === 'content' && path[6] === 'examples' && path[8] === 'value', path =>
|
---|
| 46 | // ["paths", *, *, "parameters", *, "content", *, "example"]
|
---|
| 47 | path[0] === 'paths' && path[3] === 'parameters' && path[4] === 'content' && path[7] === 'example', path =>
|
---|
| 48 | // ["paths", *, *, "parameters", *, "content", *, "examples", *, "value"]
|
---|
| 49 | path[0] === 'paths' && path[3] === 'parameters' && path[5] === 'content' && path[7] === 'examples' && path[9] === 'value'];
|
---|
| 50 | const shouldSkipResolution = path => skipResolutionTestFns.some(fn => fn(path));
|
---|
| 51 |
|
---|
| 52 | // =========================
|
---|
| 53 | // Core
|
---|
| 54 | // =========================
|
---|
| 55 |
|
---|
| 56 | /**
|
---|
| 57 | * This plugin resolves the JSON pointers.
|
---|
| 58 | * A major part of this plugin deals with cyclic references via 2 mechanisms.
|
---|
| 59 | * 1. If a pointer was already resolved before in this path, halt.
|
---|
| 60 | * 2. If the patch value points to one of the ancestors in this path, halt.
|
---|
| 61 | *
|
---|
| 62 | * Note that either one of these mechanism is sufficient, both must be in place.
|
---|
| 63 | * For examples:
|
---|
| 64 | *
|
---|
| 65 | * Given the following spec, #1 alone is insufficient because after the 2nd
|
---|
| 66 | * application, there will be a cyclic object reference.
|
---|
| 67 | * a.b.c: $ref-d
|
---|
| 68 | * d.e.f: $ref-a (per #1, safe to return patch as no immediate cycle)
|
---|
| 69 | *
|
---|
| 70 | * Given the following spec, #2 alone is insufficient because although there will
|
---|
| 71 | * never be any cyclic object reference, the plugin will keep producing patches.
|
---|
| 72 | * a: $ref-b
|
---|
| 73 | * b: $ref-a
|
---|
| 74 | */
|
---|
| 75 | const plugin = {
|
---|
| 76 | key: '$ref',
|
---|
| 77 | plugin: (ref, key, fullPath, specmap) => {
|
---|
| 78 | const specmapInstance = specmap.getInstance();
|
---|
| 79 | const parent = fullPath.slice(0, -1);
|
---|
| 80 | if (isFreelyNamed(parent) || shouldSkipResolution(parent)) {
|
---|
| 81 | return undefined;
|
---|
| 82 | }
|
---|
| 83 | const {
|
---|
| 84 | baseDoc
|
---|
| 85 | } = specmap.getContext(fullPath);
|
---|
| 86 | if (typeof ref !== 'string') {
|
---|
| 87 | return new JSONRefError('$ref: must be a string (JSON-Ref)', {
|
---|
| 88 | $ref: ref,
|
---|
| 89 | baseDoc,
|
---|
| 90 | fullPath
|
---|
| 91 | });
|
---|
| 92 | }
|
---|
| 93 | const splitString = split(ref);
|
---|
| 94 | const refPath = splitString[0];
|
---|
| 95 | const pointer = splitString[1] || '';
|
---|
| 96 | let basePath;
|
---|
| 97 | try {
|
---|
| 98 | basePath = baseDoc || refPath ? absoluteify(refPath, baseDoc) : null;
|
---|
| 99 | } catch (e) {
|
---|
| 100 | return wrapError(e, {
|
---|
| 101 | pointer,
|
---|
| 102 | $ref: ref,
|
---|
| 103 | basePath,
|
---|
| 104 | fullPath
|
---|
| 105 | });
|
---|
| 106 | }
|
---|
| 107 | let promOrVal;
|
---|
| 108 | let tokens;
|
---|
| 109 | if (pointerAlreadyInPath(pointer, basePath, parent, specmap)) {
|
---|
| 110 | // Cyclic reference!
|
---|
| 111 | // if `useCircularStructures` is not set, just leave the reference
|
---|
| 112 | // unresolved, but absolutify it so that we don't leave an invalid $ref
|
---|
| 113 | // path in the content
|
---|
| 114 | if (!specmapInstance.useCircularStructures) {
|
---|
| 115 | const absolutifiedRef = absolutifyPointer(ref, basePath);
|
---|
| 116 | if (ref === absolutifiedRef) {
|
---|
| 117 | // avoids endless looping
|
---|
| 118 | // without this, the ref plugin never stops seeing this $ref
|
---|
| 119 | return null;
|
---|
| 120 | }
|
---|
| 121 | return lib.replace(fullPath, absolutifiedRef);
|
---|
| 122 | }
|
---|
| 123 | }
|
---|
| 124 | if (basePath == null) {
|
---|
| 125 | tokens = jsonPointerToArray(pointer);
|
---|
| 126 | promOrVal = specmap.get(tokens);
|
---|
| 127 | if (typeof promOrVal === 'undefined') {
|
---|
| 128 | promOrVal = new JSONRefError(`Could not resolve reference: ${ref}`, {
|
---|
| 129 | pointer,
|
---|
| 130 | $ref: ref,
|
---|
| 131 | baseDoc,
|
---|
| 132 | fullPath
|
---|
| 133 | });
|
---|
| 134 | }
|
---|
| 135 | } else {
|
---|
| 136 | promOrVal = extractFromDoc(basePath, pointer);
|
---|
| 137 | // eslint-disable-next-line no-underscore-dangle
|
---|
| 138 | if (promOrVal.__value != null) {
|
---|
| 139 | promOrVal = promOrVal.__value; // eslint-disable-line no-underscore-dangle
|
---|
| 140 | } else {
|
---|
| 141 | promOrVal = promOrVal.catch(e => {
|
---|
| 142 | throw wrapError(e, {
|
---|
| 143 | pointer,
|
---|
| 144 | $ref: ref,
|
---|
| 145 | baseDoc,
|
---|
| 146 | fullPath
|
---|
| 147 | });
|
---|
| 148 | });
|
---|
| 149 | }
|
---|
| 150 | }
|
---|
| 151 | if (promOrVal instanceof Error) {
|
---|
| 152 | return [lib.remove(fullPath), promOrVal];
|
---|
| 153 | }
|
---|
| 154 | const absolutifiedRef = absolutifyPointer(ref, basePath);
|
---|
| 155 | const patch = lib.replace(parent, promOrVal, {
|
---|
| 156 | $$ref: absolutifiedRef
|
---|
| 157 | });
|
---|
| 158 | if (basePath && basePath !== baseDoc) {
|
---|
| 159 | return [patch, lib.context(parent, {
|
---|
| 160 | baseDoc: basePath
|
---|
| 161 | })];
|
---|
| 162 | }
|
---|
| 163 | try {
|
---|
| 164 | // prevents circular values from being constructed, unless we specifically
|
---|
| 165 | // want that to happen
|
---|
| 166 | if (!patchValueAlreadyInPath(specmap.state, patch) || specmapInstance.useCircularStructures) {
|
---|
| 167 | return patch;
|
---|
| 168 | }
|
---|
| 169 | } catch (e) {
|
---|
| 170 | // if we're catching here, path traversal failed, so we should
|
---|
| 171 | // ditch without sending any patches back up.
|
---|
| 172 | //
|
---|
| 173 | // this is a narrow fix for the larger problem of patches being queued
|
---|
| 174 | // and then having the state they were generated against be modified
|
---|
| 175 | // before they are applied.
|
---|
| 176 | //
|
---|
| 177 | // TODO: re-engineer specmap patch/state management to avoid this
|
---|
| 178 | return null;
|
---|
| 179 | }
|
---|
| 180 | return undefined;
|
---|
| 181 | }
|
---|
| 182 | };
|
---|
| 183 | const mod = Object.assign(plugin, {
|
---|
| 184 | docCache,
|
---|
| 185 | absoluteify,
|
---|
| 186 | clearCache,
|
---|
| 187 | JSONRefError,
|
---|
| 188 | wrapError,
|
---|
| 189 | getDoc,
|
---|
| 190 | split,
|
---|
| 191 | extractFromDoc,
|
---|
| 192 | fetchJSON,
|
---|
| 193 | extract,
|
---|
| 194 | jsonPointerToArray,
|
---|
| 195 | unescapeJsonPointerToken
|
---|
| 196 | });
|
---|
| 197 | export default mod;
|
---|
| 198 |
|
---|
| 199 | // =========================
|
---|
| 200 | // Utilities
|
---|
| 201 | // =========================
|
---|
| 202 |
|
---|
| 203 | /**
|
---|
| 204 | * Resolves a path and its base to an abolute URL.
|
---|
| 205 | * @api public
|
---|
| 206 | */
|
---|
| 207 | function absoluteify(path, basePath) {
|
---|
| 208 | if (!ABSOLUTE_URL_REGEXP.test(path)) {
|
---|
| 209 | if (!basePath) {
|
---|
| 210 | throw new JSONRefError(`Tried to resolve a relative URL, without having a basePath. path: '${path}' basePath: '${basePath}'`);
|
---|
| 211 | }
|
---|
| 212 | return url.resolve(basePath, path);
|
---|
| 213 | }
|
---|
| 214 | return path;
|
---|
| 215 | }
|
---|
| 216 |
|
---|
| 217 | /**
|
---|
| 218 | * Wraps an error as JSONRefError.
|
---|
| 219 | * @param {Error} e the error.
|
---|
| 220 | * @param {Object} extra (optional) optional data.
|
---|
| 221 | * @return {Error} an instance of JSONRefError.
|
---|
| 222 | * @api public
|
---|
| 223 | */
|
---|
| 224 | function wrapError(e, extra) {
|
---|
| 225 | let message;
|
---|
| 226 | if (e && e.response && e.response.body) {
|
---|
| 227 | message = `${e.response.body.code} ${e.response.body.message}`;
|
---|
| 228 | } else {
|
---|
| 229 | message = e.message;
|
---|
| 230 | }
|
---|
| 231 | return new JSONRefError(`Could not resolve reference: ${message}`, extra, e);
|
---|
| 232 | }
|
---|
| 233 |
|
---|
| 234 | /**
|
---|
| 235 | * Splits a pointer by the hash delimiter.
|
---|
| 236 | * @api public
|
---|
| 237 | */
|
---|
| 238 | function split(ref) {
|
---|
| 239 | return (ref + '').split('#'); // eslint-disable-line prefer-template
|
---|
| 240 | }
|
---|
| 241 |
|
---|
| 242 | /**
|
---|
| 243 | * Extracts a pointer from its document.
|
---|
| 244 | * @param {String} docPath the absolute document URL.
|
---|
| 245 | * @param {String} pointer the pointer whose value is to be extracted.
|
---|
| 246 | * @return {Promise} a promise of the pointer value.
|
---|
| 247 | * @api public
|
---|
| 248 | */
|
---|
| 249 | function extractFromDoc(docPath, pointer) {
|
---|
| 250 | const doc = docCache[docPath];
|
---|
| 251 | if (doc && !lib.isPromise(doc)) {
|
---|
| 252 | // If doc is already available, return __value together with the promise.
|
---|
| 253 | // __value is for special handling in cycle check:
|
---|
| 254 | // pointerAlreadyInPath() won't work if patch.value is a promise,
|
---|
| 255 | // thus when that promise is finally resolved, cycle might happen (because
|
---|
| 256 | // `spec` and `docCache[basePath]` refer to the exact same object).
|
---|
| 257 | // See test "should resolve a cyclic spec when baseDoc is specified".
|
---|
| 258 | try {
|
---|
| 259 | const v = extract(pointer, doc);
|
---|
| 260 | return Object.assign(Promise.resolve(v), {
|
---|
| 261 | __value: v
|
---|
| 262 | });
|
---|
| 263 | } catch (e) {
|
---|
| 264 | return Promise.reject(e);
|
---|
| 265 | }
|
---|
| 266 | }
|
---|
| 267 | return getDoc(docPath).then(_doc => extract(pointer, _doc));
|
---|
| 268 | }
|
---|
| 269 |
|
---|
| 270 | /**
|
---|
| 271 | * Clears all document caches.
|
---|
| 272 | * @param {String} item (optional) the name of the cache item to be cleared.
|
---|
| 273 | * @api public
|
---|
| 274 | */
|
---|
| 275 | function clearCache(item) {
|
---|
| 276 | if (typeof item !== 'undefined') {
|
---|
| 277 | delete docCache[item];
|
---|
| 278 | } else {
|
---|
| 279 | Object.keys(docCache).forEach(key => {
|
---|
| 280 | delete docCache[key];
|
---|
| 281 | });
|
---|
| 282 | }
|
---|
| 283 | }
|
---|
| 284 |
|
---|
| 285 | /**
|
---|
| 286 | * Fetches and caches a document.
|
---|
| 287 | * @param {String} docPath the absolute URL of the document.
|
---|
| 288 | * @return {Promise} a promise of the document content.
|
---|
| 289 | * @api public
|
---|
| 290 | */
|
---|
| 291 | function getDoc(docPath) {
|
---|
| 292 | const val = docCache[docPath];
|
---|
| 293 | if (val) {
|
---|
| 294 | return lib.isPromise(val) ? val : Promise.resolve(val);
|
---|
| 295 | }
|
---|
| 296 |
|
---|
| 297 | // NOTE: we need to use `mod.fetchJSON` in order to be able to overwrite it.
|
---|
| 298 | // Any tips on how to make this cleaner, please ping!
|
---|
| 299 | docCache[docPath] = mod.fetchJSON(docPath).then(doc => {
|
---|
| 300 | docCache[docPath] = doc;
|
---|
| 301 | return doc;
|
---|
| 302 | });
|
---|
| 303 | return docCache[docPath];
|
---|
| 304 | }
|
---|
| 305 |
|
---|
| 306 | /**
|
---|
| 307 | * Fetches a document.
|
---|
| 308 | * @param {String} docPath the absolute URL of the document.
|
---|
| 309 | * @return {Promise} a promise of the document content.
|
---|
| 310 | * @api public
|
---|
| 311 | */
|
---|
| 312 | function fetchJSON(docPath) {
|
---|
| 313 | return fetch(docPath, {
|
---|
| 314 | headers: {
|
---|
| 315 | Accept: ACCEPT_HEADER_VALUE_FOR_DOCUMENTS
|
---|
| 316 | },
|
---|
| 317 | loadSpec: true
|
---|
| 318 | }).then(res => res.text()).then(text => jsYaml.load(text));
|
---|
| 319 | }
|
---|
| 320 |
|
---|
| 321 | /**
|
---|
| 322 | * Extracts a pointer from an object.
|
---|
| 323 | * @param {String[]} pointer the JSON pointer.
|
---|
| 324 | * @param {Object} obj an object whose value is to be extracted.
|
---|
| 325 | * @return {Object} the value to be extracted.
|
---|
| 326 | * @api public
|
---|
| 327 | */
|
---|
| 328 | function extract(pointer, obj) {
|
---|
| 329 | const tokens = jsonPointerToArray(pointer);
|
---|
| 330 | if (tokens.length < 1) {
|
---|
| 331 | return obj;
|
---|
| 332 | }
|
---|
| 333 | const val = lib.getIn(obj, tokens);
|
---|
| 334 | if (typeof val === 'undefined') {
|
---|
| 335 | throw new JSONRefError(`Could not resolve pointer: ${pointer} does not exist in document`, {
|
---|
| 336 | pointer
|
---|
| 337 | });
|
---|
| 338 | }
|
---|
| 339 | return val;
|
---|
| 340 | }
|
---|
| 341 |
|
---|
| 342 | /**
|
---|
| 343 | * Converts a JSON pointer to array.
|
---|
| 344 | * @api public
|
---|
| 345 | */
|
---|
| 346 | function jsonPointerToArray(pointer) {
|
---|
| 347 | if (typeof pointer !== 'string') {
|
---|
| 348 | throw new TypeError(`Expected a string, got a ${typeof pointer}`);
|
---|
| 349 | }
|
---|
| 350 | if (pointer[0] === '/') {
|
---|
| 351 | pointer = pointer.substr(1);
|
---|
| 352 | }
|
---|
| 353 | if (pointer === '') {
|
---|
| 354 | return [];
|
---|
| 355 | }
|
---|
| 356 | return pointer.split('/').map(unescapeJsonPointerToken);
|
---|
| 357 | }
|
---|
| 358 |
|
---|
| 359 | /**
|
---|
| 360 | * Unescapes a JSON pointer.
|
---|
| 361 | * @api public
|
---|
| 362 | */
|
---|
| 363 | function unescapeJsonPointerToken(token) {
|
---|
| 364 | if (typeof token !== 'string') {
|
---|
| 365 | return token;
|
---|
| 366 | }
|
---|
| 367 | const params = new URLSearchParams(`=${token.replace(/~1/g, '/').replace(/~0/g, '~')}`);
|
---|
| 368 | return params.get('');
|
---|
| 369 | }
|
---|
| 370 |
|
---|
| 371 | /**
|
---|
| 372 | * Escapes a JSON pointer.
|
---|
| 373 | * @api public
|
---|
| 374 | */
|
---|
| 375 | function escapeJsonPointerToken(token) {
|
---|
| 376 | const params = new URLSearchParams([['', token.replace(/~/g, '~0').replace(/\//g, '~1')]]);
|
---|
| 377 | return params.toString().slice(1);
|
---|
| 378 | }
|
---|
| 379 | function arrayToJsonPointer(arr) {
|
---|
| 380 | if (arr.length === 0) {
|
---|
| 381 | return '';
|
---|
| 382 | }
|
---|
| 383 | return `/${arr.map(escapeJsonPointerToken).join('/')}`;
|
---|
| 384 | }
|
---|
| 385 | const pointerBoundaryChar = c => !c || c === '/' || c === '#';
|
---|
| 386 | function pointerIsAParent(pointer, parentPointer) {
|
---|
| 387 | if (pointerBoundaryChar(parentPointer)) {
|
---|
| 388 | // This is the root of the document, so its naturally a parent
|
---|
| 389 | return true;
|
---|
| 390 | }
|
---|
| 391 | const nextChar = pointer.charAt(parentPointer.length);
|
---|
| 392 | const lastParentChar = parentPointer.slice(-1);
|
---|
| 393 | return pointer.indexOf(parentPointer) === 0 && (!nextChar || nextChar === '/' || nextChar === '#') && lastParentChar !== '#';
|
---|
| 394 | }
|
---|
| 395 |
|
---|
| 396 | // =========================
|
---|
| 397 | // Private
|
---|
| 398 | // =========================
|
---|
| 399 |
|
---|
| 400 | /**
|
---|
| 401 | * Checks if this pointer points back to one or more pointers along the path.
|
---|
| 402 | */
|
---|
| 403 | function pointerAlreadyInPath(pointer, basePath, parent, specmap) {
|
---|
| 404 | let refs = specmapRefs.get(specmap);
|
---|
| 405 | if (!refs) {
|
---|
| 406 | // Stores all resolved references of a specmap instance.
|
---|
| 407 | // Schema: path -> pointer (path's $ref value).
|
---|
| 408 | refs = {};
|
---|
| 409 | specmapRefs.set(specmap, refs);
|
---|
| 410 | }
|
---|
| 411 | const parentPointer = arrayToJsonPointer(parent);
|
---|
| 412 | const fullyQualifiedPointer = `${basePath || '<specmap-base>'}#${pointer}`;
|
---|
| 413 |
|
---|
| 414 | // dirty hack to strip `allof/[index]` from the path, in order to avoid cases
|
---|
| 415 | // where we get false negatives because:
|
---|
| 416 | // - we resolve a path, then
|
---|
| 417 | // - allOf plugin collapsed `allOf/[index]` out of the path, then
|
---|
| 418 | // - we try to work on a child $ref within that collapsed path.
|
---|
| 419 | //
|
---|
| 420 | // because of the path collapse, we lose track of it in our specmapRefs hash
|
---|
| 421 | // solution: always throw the allOf constructs out of paths we store
|
---|
| 422 | // TODO: solve this with a global register, or by writing more metadata in
|
---|
| 423 | // either allOf or refs plugin
|
---|
| 424 | const safeParentPointer = parentPointer.replace(/allOf\/\d+\/?/g, '');
|
---|
| 425 |
|
---|
| 426 | // Case 1: direct cycle, e.g. a.b.c.$ref: '/a.b'
|
---|
| 427 | // Detect by checking that the parent path doesn't start with pointer.
|
---|
| 428 | // This only applies if the pointer is internal, i.e. basePath === rootPath (could be null)
|
---|
| 429 | const rootDoc = specmap.contextTree.get([]).baseDoc;
|
---|
| 430 | if (basePath === rootDoc && pointerIsAParent(safeParentPointer, pointer)) {
|
---|
| 431 | // eslint-disable-line
|
---|
| 432 | return true;
|
---|
| 433 | }
|
---|
| 434 |
|
---|
| 435 | // Case 2: indirect cycle
|
---|
| 436 | // ex1: a.$ref: '/b' & b.c.$ref: '/b/c'
|
---|
| 437 | // ex2: a.$ref: '/b/c' & b.c.$ref: '/b'
|
---|
| 438 | // Detect by retrieving all the $refs along the path of parent
|
---|
| 439 | // and checking if any starts with pointer or vice versa.
|
---|
| 440 | let currPath = '';
|
---|
| 441 | const hasIndirectCycle = parent.some(token => {
|
---|
| 442 | currPath = `${currPath}/${escapeJsonPointerToken(token)}`;
|
---|
| 443 | return refs[currPath] && refs[currPath].some(ref => pointerIsAParent(ref, fullyQualifiedPointer) || pointerIsAParent(fullyQualifiedPointer, ref));
|
---|
| 444 | });
|
---|
| 445 | if (hasIndirectCycle) {
|
---|
| 446 | return true;
|
---|
| 447 | }
|
---|
| 448 |
|
---|
| 449 | // No cycle, this ref will be resolved, so stores it now for future detection.
|
---|
| 450 | // No need to store if has cycle, as parent path is a dead-end and won't be checked again.
|
---|
| 451 |
|
---|
| 452 | refs[safeParentPointer] = (refs[safeParentPointer] || []).concat(fullyQualifiedPointer);
|
---|
| 453 | return undefined;
|
---|
| 454 | }
|
---|
| 455 |
|
---|
| 456 | /**
|
---|
| 457 | * Checks if the value of this patch ends up pointing to an ancestor along the path.
|
---|
| 458 | */
|
---|
| 459 | function patchValueAlreadyInPath(root, patch) {
|
---|
| 460 | const ancestors = [root];
|
---|
| 461 | patch.path.reduce((parent, p) => {
|
---|
| 462 | ancestors.push(parent[p]);
|
---|
| 463 | return parent[p];
|
---|
| 464 | }, root);
|
---|
| 465 | return pointToAncestor(patch.value);
|
---|
| 466 | function pointToAncestor(obj) {
|
---|
| 467 | return lib.isObject(obj) && (ancestors.indexOf(obj) >= 0 || Object.keys(obj).some(k => pointToAncestor(obj[k])));
|
---|
| 468 | }
|
---|
| 469 | } |
---|