source: node_modules/@swagger-api/apidom-reference/es/dereference/strategies/openapi-2/visitor.mjs

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: 16.4 KB
Line 
1import stampit from 'stampit';
2import { propEq } from 'ramda';
3import { isPrimitiveElement, isStringElement, isMemberElement, isElement, IdentityManager, visit, cloneShallow, cloneDeep, toValue } from '@swagger-api/apidom-core';
4import { ApiDOMError } from '@swagger-api/apidom-error';
5import { evaluate, uriToPointer } from '@swagger-api/apidom-json-pointer';
6import { getNodeType, isReferenceLikeElement, isJSONReferenceLikeElement, keyMap, ReferenceElement, PathItemElement } from '@swagger-api/apidom-ns-openapi-2';
7import MaximumDereferenceDepthError from "../../../errors/MaximumDereferenceDepthError.mjs";
8import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
9import { AncestorLineage } from "../../util.mjs";
10import * as url from "../../../util/url.mjs";
11import parse from "../../../parse/index.mjs";
12import Reference from "../../../Reference.mjs"; // @ts-ignore
13const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];
14
15// initialize element identity manager
16const identityManager = IdentityManager();
17
18/**
19 * Predicate for detecting if element was created by merging referencing
20 * element with particular element identity with a referenced element.
21 */
22const wasReferencedBy = referencingElement => element => element.meta.hasKey('ref-referencing-element-id') && element.meta.get('ref-referencing-element-id').equals(toValue(identityManager.identify(referencingElement)));
23const OpenApi2DereferenceVisitor = stampit({
24 props: {
25 indirections: [],
26 namespace: null,
27 reference: null,
28 options: null,
29 ancestors: null
30 },
31 init({
32 indirections = [],
33 reference,
34 namespace,
35 options,
36 ancestors = new AncestorLineage()
37 }) {
38 this.indirections = indirections;
39 this.namespace = namespace;
40 this.reference = reference;
41 this.options = options;
42 this.ancestors = new AncestorLineage(...ancestors);
43 },
44 methods: {
45 toBaseURI(uri) {
46 return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
47 },
48 async toReference(uri) {
49 // detect maximum depth of resolution
50 if (this.reference.depth >= this.options.resolve.maxDepth) {
51 throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`);
52 }
53 const baseURI = this.toBaseURI(uri);
54 const {
55 refSet
56 } = this.reference;
57
58 // we've already processed this Reference in past
59 if (refSet.has(baseURI)) {
60 return refSet.find(propEq(baseURI, 'uri'));
61 }
62 const parseResult = await parse(url.unsanitize(baseURI), {
63 ...this.options,
64 parse: {
65 ...this.options.parse,
66 mediaType: 'text/plain'
67 }
68 });
69
70 // register new Reference with ReferenceSet
71 const reference = Reference({
72 uri: baseURI,
73 value: parseResult,
74 depth: this.reference.depth + 1
75 });
76 refSet.add(reference);
77 return reference;
78 },
79 toAncestorLineage(ancestors) {
80 /**
81 * Compute full ancestors lineage.
82 * Ancestors are flatten to unwrap all Element instances.
83 */
84 const directAncestors = new Set(ancestors.filter(isElement));
85 const ancestorsLineage = new AncestorLineage(...this.ancestors, directAncestors);
86 return [ancestorsLineage, directAncestors];
87 },
88 async ReferenceElement(referencingElement, key, parent, path, ancestors) {
89 const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]);
90
91 // detect possible cycle in traversal and avoid it
92 if (ancestorsLineage.includesCycle(referencingElement)) {
93 return false;
94 }
95 const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref));
96
97 // ignore resolving external Reference Objects
98 if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) {
99 // skip traversing this reference element and all it's child elements
100 return false;
101 }
102 const reference = await this.toReference(toValue(referencingElement.$ref));
103 const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref));
104 this.indirections.push(referencingElement);
105 const jsonPointer = uriToPointer($refBaseURI);
106
107 // possibly non-semantic fragment
108 let referencedElement = evaluate(jsonPointer, reference.value.result);
109
110 // applying semantics to a fragment
111 if (isPrimitiveElement(referencedElement)) {
112 const referencedElementType = toValue(referencingElement.meta.get('referenced-element'));
113 if (isReferenceLikeElement(referencedElement)) {
114 // handling indirect references
115 referencedElement = ReferenceElement.refract(referencedElement);
116 referencedElement.setMetaProperty('referenced-element', referencedElementType);
117 } else {
118 // handling direct references
119 const ElementClass = this.namespace.getElementClass(referencedElementType);
120 referencedElement = ElementClass.refract(referencedElement);
121 }
122 }
123
124 // detect direct or circular reference
125 if (this.indirections.includes(referencedElement)) {
126 throw new ApiDOMError('Recursive Reference Object detected');
127 }
128
129 // detect maximum depth of dereferencing
130 if (this.indirections.length > this.options.dereference.maxDepth) {
131 throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
132 }
133
134 // append referencing reference to ancestors lineage
135 directAncestors.add(referencingElement);
136
137 // dive deep into the fragment
138 const visitor = OpenApi2DereferenceVisitor({
139 reference,
140 namespace: this.namespace,
141 indirections: [...this.indirections],
142 options: this.options,
143 ancestors: ancestorsLineage
144 });
145 referencedElement = await visitAsync(referencedElement, visitor, {
146 keyMap,
147 nodeTypeGetter: getNodeType
148 });
149
150 // remove referencing reference from ancestors lineage
151 directAncestors.delete(referencingElement);
152 this.indirections.pop();
153 const mergeAndAnnotateReferencedElement = refedElement => {
154 const copy = cloneShallow(refedElement);
155
156 // annotate referenced element with info about original referencing element
157 copy.setMetaProperty('ref-fields', {
158 // @ts-ignore
159 $ref: toValue(referencingElement.$ref)
160 });
161 // annotate fragment with info about origin
162 copy.setMetaProperty('ref-origin', reference.uri);
163 // annotate fragment with info about referencing element
164 copy.setMetaProperty('ref-referencing-element-id', cloneDeep(identityManager.identify(referencingElement)));
165 return copy;
166 };
167
168 // attempting to create cycle
169 if (ancestorsLineage.includes(referencingElement) || ancestorsLineage.includes(referencedElement)) {
170 var _ancestorsLineage$fin;
171 const replaceWith = (_ancestorsLineage$fin = ancestorsLineage.findItem(wasReferencedBy(referencingElement))) !== null && _ancestorsLineage$fin !== void 0 ? _ancestorsLineage$fin : mergeAndAnnotateReferencedElement(referencedElement);
172 if (isMemberElement(parent)) {
173 parent.value = replaceWith; // eslint-disable-line no-param-reassign
174 } else if (Array.isArray(parent)) {
175 parent[key] = replaceWith; // eslint-disable-line no-param-reassign
176 }
177 return false;
178 }
179
180 // transclude referencing element with merged referenced element
181 return mergeAndAnnotateReferencedElement(referencedElement);
182 },
183 async PathItemElement(referencingElement, key, parent, path, ancestors) {
184 const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]);
185
186 // ignore PathItemElement without $ref field
187 if (!isStringElement(referencingElement.$ref)) {
188 return undefined;
189 }
190
191 // detect possible cycle in traversal and avoid it
192 if (ancestorsLineage.includesCycle(referencingElement)) {
193 return false;
194 }
195 const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref));
196
197 // ignore resolving external Path Item Objects
198 if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) {
199 // skip traversing this Path Item element but traverse all it's child elements
200 return undefined;
201 }
202 const reference = await this.toReference(toValue(referencingElement.$ref));
203 const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref));
204 this.indirections.push(referencingElement);
205 const jsonPointer = uriToPointer($refBaseURI);
206
207 // possibly non-semantic referenced element
208 let referencedElement = evaluate(jsonPointer, reference.value.result);
209
210 // applying semantics to a referenced element
211 if (isPrimitiveElement(referencedElement)) {
212 referencedElement = PathItemElement.refract(referencedElement);
213 }
214
215 // detect direct or indirect reference
216 if (this.indirections.includes(referencedElement)) {
217 throw new ApiDOMError('Recursive Path Item Object reference detected');
218 }
219
220 // detect maximum depth of dereferencing
221 if (this.indirections.length > this.options.dereference.maxDepth) {
222 throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
223 }
224
225 // append referencing path item to ancestors lineage
226 directAncestors.add(referencingElement);
227
228 // dive deep into the referenced element
229 const visitor = OpenApi2DereferenceVisitor({
230 reference,
231 namespace: this.namespace,
232 indirections: [...this.indirections],
233 options: this.options,
234 ancestors: ancestorsLineage
235 });
236 referencedElement = await visitAsync(referencedElement, visitor, {
237 keyMap,
238 nodeTypeGetter: getNodeType
239 });
240
241 // remove referencing path item from ancestors lineage
242 directAncestors.delete(referencingElement);
243 this.indirections.pop();
244 const mergeAndAnnotateReferencedElement = refedElement => {
245 // merge fields from referenced Path Item with referencing one
246 const mergedElement = new PathItemElement([...refedElement.content], cloneDeep(referencedElement.meta), cloneDeep(referencedElement.attributes));
247 // existing keywords from referencing PathItemElement overrides ones from referenced element
248 referencingElement.forEach((value, keyElement, item) => {
249 mergedElement.remove(toValue(keyElement));
250 mergedElement.content.push(item);
251 });
252 mergedElement.remove('$ref');
253
254 // annotate referenced element with info about original referencing element
255 mergedElement.setMetaProperty('ref-fields', {
256 $ref: toValue(referencingElement.$ref)
257 });
258 // annotate referenced element with info about origin
259 mergedElement.setMetaProperty('ref-origin', reference.uri);
260 // annotate fragment with info about referencing element
261 mergedElement.setMetaProperty('ref-referencing-element-id', cloneDeep(identityManager.identify(referencingElement)));
262 return mergedElement;
263 };
264
265 // attempting to create cycle
266 if (ancestorsLineage.includes(referencingElement) || ancestorsLineage.includes(referencedElement)) {
267 var _ancestorsLineage$fin2;
268 const replaceWith = (_ancestorsLineage$fin2 = ancestorsLineage.findItem(wasReferencedBy(referencingElement))) !== null && _ancestorsLineage$fin2 !== void 0 ? _ancestorsLineage$fin2 : mergeAndAnnotateReferencedElement(referencedElement);
269 if (isMemberElement(parent)) {
270 parent.value = replaceWith; // eslint-disable-line no-param-reassign
271 } else if (Array.isArray(parent)) {
272 parent[key] = replaceWith; // eslint-disable-line no-param-reassign
273 }
274 return false;
275 }
276
277 // transclude referencing element with merged referenced element
278 return mergeAndAnnotateReferencedElement(referencedElement);
279 },
280 async JSONReferenceElement(referencingElement, key, parent, path, ancestors) {
281 const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]);
282
283 // detect possible cycle in traversal and avoid it
284 if (ancestorsLineage.includesCycle(referencingElement)) {
285 return false;
286 }
287 const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref));
288
289 // ignore resolving external JSONReference Objects
290 if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) {
291 // skip traversing this JSONReference element and all it's child elements
292 return false;
293 }
294 const reference = await this.toReference(toValue(referencingElement.$ref));
295 const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref));
296 this.indirections.push(referencingElement);
297 const jsonPointer = uriToPointer($refBaseURI);
298
299 // possibly non-semantic fragment
300 let referencedElement = evaluate(jsonPointer, reference.value.result);
301
302 // applying semantics to a fragment
303 if (isPrimitiveElement(referencedElement)) {
304 const referencedElementType = toValue(referencingElement.meta.get('referenced-element'));
305 if (isJSONReferenceLikeElement(referencedElement)) {
306 // handling indirect references
307 referencedElement = ReferenceElement.refract(referencedElement);
308 referencedElement.setMetaProperty('referenced-element', referencedElementType);
309 } else {
310 // handling direct references
311 const ElementClass = this.namespace.getElementClass(referencedElementType);
312 referencedElement = ElementClass.refract(referencedElement);
313 }
314 }
315
316 // detect direct or circular reference
317 if (this.indirections.includes(referencedElement)) {
318 throw new ApiDOMError('Recursive Reference Object detected');
319 }
320
321 // detect maximum depth of dereferencing
322 if (this.indirections.length > this.options.dereference.maxDepth) {
323 throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
324 }
325
326 // append referencing reference to ancestors lineage
327 directAncestors.add(referencingElement);
328
329 // dive deep into the fragment
330 const visitor = OpenApi2DereferenceVisitor({
331 reference,
332 namespace: this.namespace,
333 indirections: [...this.indirections],
334 options: this.options,
335 ancestors: ancestorsLineage
336 });
337 referencedElement = await visitAsync(referencedElement, visitor, {
338 keyMap,
339 nodeTypeGetter: getNodeType
340 });
341
342 // remove referencing reference from ancestors lineage
343 directAncestors.delete(referencingElement);
344 this.indirections.pop();
345 const mergeAndAnnotateReferencedElement = refedElement => {
346 const copy = cloneShallow(refedElement);
347
348 // annotate referenced element with info about original referencing element
349 copy.setMetaProperty('ref-fields', {
350 // @ts-ignore
351 $ref: toValue(referencingElement.$ref)
352 });
353 // annotate fragment with info about origin
354 copy.setMetaProperty('ref-origin', reference.uri);
355 // annotate fragment with info about referencing element
356 copy.setMetaProperty('ref-referencing-element-id', cloneDeep(identityManager.identify(referencingElement)));
357 return copy;
358 };
359
360 // attempting to create cycle
361 if (ancestorsLineage.includes(referencingElement) || ancestorsLineage.includes(referencedElement)) {
362 var _ancestorsLineage$fin3;
363 const replaceWith = (_ancestorsLineage$fin3 = ancestorsLineage.findItem(wasReferencedBy(referencingElement))) !== null && _ancestorsLineage$fin3 !== void 0 ? _ancestorsLineage$fin3 : mergeAndAnnotateReferencedElement(referencedElement);
364 if (isMemberElement(parent)) {
365 parent.value = replaceWith; // eslint-disable-line no-param-reassign
366 } else if (Array.isArray(parent)) {
367 parent[key] = replaceWith; // eslint-disable-line no-param-reassign
368 }
369 return false;
370 }
371
372 // transclude referencing element with merged referenced element
373 return mergeAndAnnotateReferencedElement(referencedElement);
374 }
375 }
376});
377export default OpenApi2DereferenceVisitor;
Note: See TracBrowser for help on using the repository browser.