UNPKG

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