source: node_modules/swagger-client/es/specmap/lib/refs.js

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 15.7 KB
Line 
1import jsYaml from 'js-yaml';
2import { url } from '@swagger-api/apidom-reference/configuration/empty';
3import '../../helpers/fetch-polyfill.node.js';
4import lib from './index.js';
5import createError from './create-error.js';
6import { isFreelyNamed, absolutifyPointer } from '../helpers.js';
7import { ACCEPT_HEADER_VALUE_FOR_DOCUMENTS } from '../../constants.js';
8const ABSOLUTE_URL_REGEXP = /^([a-z]+:\/\/|\/\/)/i;
9const JSONRefError = createError('JSONRefError', function cb(message, extra, oriError) {
10 this.originalError = oriError;
11 Object.assign(this, extra || {});
12});
13const docCache = {};
14const specmapRefs = new WeakMap();
15const skipResolutionTestFns = [
16// OpenAPI 2.0 response examples
17path =>
18// ["paths", *, *, "responses", *, "examples"]
19path[0] === 'paths' && path[3] === 'responses' && path[5] === 'examples',
20// OpenAPI 3.0 Response Media Type Examples
21path =>
22// ["paths", *, *, "responses", *, "content", *, "example"]
23path[0] === 'paths' && path[3] === 'responses' && path[5] === 'content' && path[7] === 'example', path =>
24// ["paths", *, *, "responses", *, "content", *, "examples", *, "value"]
25path[0] === 'paths' && path[3] === 'responses' && path[5] === 'content' && path[7] === 'examples' && path[9] === 'value',
26// OpenAPI 3.0 Request Body Media Type Examples
27path =>
28// ["paths", *, *, "requestBody", "content", *, "example"]
29path[0] === 'paths' && path[3] === 'requestBody' && path[4] === 'content' && path[6] === 'example', path =>
30// ["paths", *, *, "requestBody", "content", *, "examples", *, "value"]
31path[0] === 'paths' && path[3] === 'requestBody' && path[4] === 'content' && path[6] === 'examples' && path[8] === 'value',
32// OAS 3.0 Parameter Examples
33path =>
34// ["paths", *, "parameters", *, "example"]
35path[0] === 'paths' && path[2] === 'parameters' && path[4] === 'example', path =>
36// ["paths", *, *, "parameters", *, "example"]
37path[0] === 'paths' && path[3] === 'parameters' && path[5] === 'example', path =>
38// ["paths", *, "parameters", *, "examples", *, "value"]
39path[0] === 'paths' && path[2] === 'parameters' && path[4] === 'examples' && path[6] === 'value', path =>
40// ["paths", *, *, "parameters", *, "examples", *, "value"]
41path[0] === 'paths' && path[3] === 'parameters' && path[5] === 'examples' && path[7] === 'value', path =>
42// ["paths", *, "parameters", *, "content", *, "example"]
43path[0] === 'paths' && path[2] === 'parameters' && path[4] === 'content' && path[6] === 'example', path =>
44// ["paths", *, "parameters", *, "content", *, "examples", *, "value"]
45path[0] === 'paths' && path[2] === 'parameters' && path[4] === 'content' && path[6] === 'examples' && path[8] === 'value', path =>
46// ["paths", *, *, "parameters", *, "content", *, "example"]
47path[0] === 'paths' && path[3] === 'parameters' && path[4] === 'content' && path[7] === 'example', path =>
48// ["paths", *, *, "parameters", *, "content", *, "examples", *, "value"]
49path[0] === 'paths' && path[3] === 'parameters' && path[5] === 'content' && path[7] === 'examples' && path[9] === 'value'];
50const 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 */
75const 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};
183const 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});
197export default mod;
198
199// =========================
200// Utilities
201// =========================
202
203/**
204 * Resolves a path and its base to an abolute URL.
205 * @api public
206 */
207function 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 */
224function 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 */
238function 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 */
249function 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 */
275function 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 */
291function 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 */
312function 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 */
328function 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 */
346function 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 */
363function 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 */
375function escapeJsonPointerToken(token) {
376 const params = new URLSearchParams([['', token.replace(/~/g, '~0').replace(/\//g, '~1')]]);
377 return params.toString().slice(1);
378}
379function arrayToJsonPointer(arr) {
380 if (arr.length === 0) {
381 return '';
382 }
383 return `/${arr.map(escapeJsonPointerToken).join('/')}`;
384}
385const pointerBoundaryChar = c => !c || c === '/' || c === '#';
386function 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 */
403function 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 */
459function 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}
Note: See TracBrowser for help on using the repository browser.