source: node_modules/@swagger-api/apidom-reference/es/resolve/strategies/openapi-3-1/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: 17.6 KB
Line 
1import stampit from 'stampit';
2import { propEq, values, has, pipe, none } from 'ramda';
3import { allP } from 'ramda-adjunct';
4import { isPrimitiveElement, isStringElement, visit, toValue } from '@swagger-api/apidom-core';
5import { ApiDOMError } from '@swagger-api/apidom-error';
6import { evaluate as jsonPointerEvaluate, uriToPointer } from '@swagger-api/apidom-json-pointer';
7import { getNodeType, isReferenceElement, isReferenceLikeElement, keyMap, ReferenceElement, PathItemElement, isSchemaElement, isPathItemElement } from '@swagger-api/apidom-ns-openapi-3-1';
8import MaximumDereferenceDepthError from "../../../errors/MaximumDereferenceDepthError.mjs";
9import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
10import EvaluationJsonSchemaUriError from "../../../errors/EvaluationJsonSchemaUriError.mjs";
11import * as url from "../../../util/url.mjs";
12import parse from "../../../parse/index.mjs";
13import Reference from "../../../Reference.mjs";
14import File from "../../../util/File.mjs";
15import { evaluate as uriEvaluate } from "../../../dereference/strategies/openapi-3-1/selectors/uri.mjs";
16import { maybeRefractToSchemaElement, resolveSchema$refField } from "./util.mjs";
17import { evaluate as $anchorEvaluate, isAnchor, uriToAnchor } from "../../../dereference/strategies/openapi-3-1/selectors/$anchor.mjs"; // @ts-ignore
18const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];
19
20// eslint-disable-next-line @typescript-eslint/naming-convention
21const OpenApi3_1ResolveVisitor = stampit({
22 props: {
23 indirections: [],
24 namespace: null,
25 reference: null,
26 crawledElements: null,
27 crawlingMap: null,
28 visited: null,
29 options: null
30 },
31 init({
32 reference,
33 namespace,
34 indirections = [],
35 visited = new WeakSet(),
36 options
37 }) {
38 this.indirections = indirections;
39 this.namespace = namespace;
40 this.reference = reference;
41 this.crawledElements = [];
42 this.crawlingMap = {};
43 this.visited = visited;
44 this.options = options;
45 },
46 methods: {
47 toBaseURI(uri) {
48 return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
49 },
50 async toReference(uri) {
51 // detect maximum depth of resolution
52 if (this.reference.depth >= this.options.resolve.maxDepth) {
53 throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`);
54 }
55 const baseURI = this.toBaseURI(uri);
56 const {
57 refSet
58 } = this.reference;
59
60 // we've already processed this Reference in past
61 if (refSet.has(baseURI)) {
62 return refSet.find(propEq(baseURI, 'uri'));
63 }
64 const parseResult = await parse(url.unsanitize(baseURI), {
65 ...this.options,
66 parse: {
67 ...this.options.parse,
68 mediaType: 'text/plain'
69 }
70 });
71
72 // register new Reference with ReferenceSet
73 const reference = Reference({
74 uri: baseURI,
75 value: parseResult,
76 depth: this.reference.depth + 1
77 });
78 refSet.add(reference);
79 return reference;
80 },
81 ReferenceElement(referenceElement) {
82 const uri = toValue(referenceElement.$ref);
83 const retrievalURI = this.toBaseURI(uri);
84
85 // ignore resolving external Reference Objects
86 if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) {
87 // skip traversing this reference element and all it's child elements
88 return false;
89 }
90 if (!has(retrievalURI, this.crawlingMap)) {
91 this.crawlingMap[retrievalURI] = this.toReference(uri);
92 }
93 this.crawledElements.push(referenceElement);
94 return undefined;
95 },
96 PathItemElement(pathItemElement) {
97 // ignore PathItemElement without $ref field
98 if (!isStringElement(pathItemElement.$ref)) {
99 return undefined;
100 }
101 const uri = toValue(pathItemElement.$ref);
102 const retrievalURI = this.toBaseURI(uri);
103
104 // ignore resolving external Path Item Objects
105 if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) {
106 // skip traversing this Path Item element but traverse all it's child elements
107 return undefined;
108 }
109 if (!has(retrievalURI, this.crawlingMap)) {
110 this.crawlingMap[retrievalURI] = this.toReference(uri);
111 }
112 this.crawledElements.push(pathItemElement);
113 return undefined;
114 },
115 LinkElement(linkElement) {
116 // ignore LinkElement without operationRef or operationId field
117 if (!isStringElement(linkElement.operationRef) && !isStringElement(linkElement.operationId)) {
118 return undefined;
119 }
120 const uri = toValue(linkElement.operationRef);
121 const retrievalURI = this.toBaseURI(uri);
122
123 // ignore resolving external Path Item Elements
124 const isExternal = url.stripHash(this.reference.uri) !== retrievalURI;
125 if (!this.options.resolve.external && isExternal) {
126 return undefined;
127 }
128
129 // operationRef and operationId are mutually exclusive
130 if (isStringElement(linkElement.operationRef) && isStringElement(linkElement.operationId)) {
131 throw new ApiDOMError('LinkElement operationRef and operationId are mutually exclusive.');
132 }
133 if (isExternal) {
134 if (!has(retrievalURI, this.crawlingMap)) {
135 this.crawlingMap[retrievalURI] = this.toReference(uri);
136 }
137 }
138 return undefined;
139 },
140 ExampleElement(exampleElement) {
141 // ignore ExampleElement without externalValue field
142 if (!isStringElement(exampleElement.externalValue)) {
143 return undefined;
144 }
145
146 // value and externalValue fields are mutually exclusive
147 if (exampleElement.hasKey('value') && isStringElement(exampleElement.externalValue)) {
148 throw new ApiDOMError('ExampleElement value and externalValue fields are mutually exclusive.');
149 }
150 const uri = toValue(exampleElement.externalValue);
151 const retrievalURI = this.toBaseURI(uri);
152
153 // ignore resolving external Example Objects
154 if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) {
155 // skip traversing this Example element but traverse all it's child elements
156 return undefined;
157 }
158 if (!has(retrievalURI, this.crawlingMap)) {
159 this.crawlingMap[retrievalURI] = this.toReference(uri);
160 }
161 return undefined;
162 },
163 async SchemaElement(schemaElement) {
164 /**
165 * Skip traversal for already visited schemas and all their child schemas.
166 * visit function detects cycles in path automatically.
167 */
168 if (this.visited.has(schemaElement)) {
169 return false;
170 }
171 // skip current referencing schema as $ref keyword was not defined
172 if (!isStringElement(schemaElement.$ref)) {
173 // mark current referencing schema as visited
174 this.visited.add(schemaElement);
175 // skip traversing this schema but traverse all it's child schemas
176 return undefined;
177 }
178
179 // compute baseURI using rules around $id and $ref keywords
180 const reference = await this.toReference(url.unsanitize(this.reference.uri));
181 let {
182 uri: retrievalURI
183 } = reference;
184 const $refBaseURI = resolveSchema$refField(retrievalURI, schemaElement);
185 const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
186 const file = File({
187 uri: $refBaseURIStrippedHash
188 });
189 const isUnknownURI = none(r => r.canRead(file), this.options.resolve.resolvers);
190 const isURL = !isUnknownURI;
191 const isExternalURL = uri => url.stripHash(this.reference.uri) !== uri;
192 if (!has($refBaseURIStrippedHash, this.crawlingMap)) {
193 try {
194 if (isUnknownURI || isURL) {
195 this.crawlingMap[$refBaseURIStrippedHash] = reference;
196 } else {
197 retrievalURI = this.toBaseURI(toValue($refBaseURI));
198
199 // ignore resolving external Schema Objects
200 if (!this.options.resolve.external && isExternalURL(retrievalURI)) {
201 // skip traversing this schema element but traverse all it's child elements
202 this.visited.add(schemaElement);
203 return undefined;
204 }
205 this.crawlingMap[$refBaseURIStrippedHash] = this.toReference(url.unsanitize($refBaseURI));
206 }
207 } catch (error) {
208 if (isURL && error instanceof EvaluationJsonSchemaUriError) {
209 retrievalURI = this.toBaseURI(url.unsanitize($refBaseURI));
210
211 // ignore resolving external Schema Objects
212 if (!this.options.resolve.external && isExternalURL(retrievalURI)) {
213 // skip traversing this schema element but traverse all it's child elements
214 this.visited.add(schemaElement);
215 return undefined;
216 }
217 this.crawlingMap[$refBaseURIStrippedHash] = this.toReference(url.unsanitize($refBaseURI));
218 } else {
219 throw error;
220 }
221 }
222 }
223 this.crawledElements.push(schemaElement);
224 return undefined;
225 },
226 async crawlReferenceElement(referenceElement) {
227 // @ts-ignore
228 const reference = await this.toReference(toValue(referenceElement.$ref));
229 this.indirections.push(referenceElement);
230 const jsonPointer = uriToPointer(toValue(referenceElement.$ref));
231
232 // possibly non-semantic fragment
233 let fragment = jsonPointerEvaluate(jsonPointer, reference.value.result);
234
235 // applying semantics to a fragment
236 if (isPrimitiveElement(fragment)) {
237 const referencedElementType = toValue(referenceElement.meta.get('referenced-element'));
238 if (isReferenceLikeElement(fragment)) {
239 // handling indirect references
240 fragment = ReferenceElement.refract(fragment);
241 fragment.setMetaProperty('referenced-element', referencedElementType);
242 } else {
243 // handling direct references
244 const ElementClass = this.namespace.getElementClass(referencedElementType);
245 fragment = ElementClass.refract(fragment);
246 }
247 }
248
249 // detect direct or circular reference
250 if (this.indirections.includes(fragment)) {
251 throw new ApiDOMError('Recursive Reference Object detected');
252 }
253
254 // detect maximum depth of dereferencing
255 if (this.indirections.length > this.options.dereference.maxDepth) {
256 throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
257 }
258
259 // dive deep into the fragment
260 const visitor = OpenApi3_1ResolveVisitor({
261 reference,
262 namespace: this.namespace,
263 indirections: [...this.indirections],
264 options: this.options
265 });
266 await visitAsync(fragment, visitor, {
267 keyMap,
268 nodeTypeGetter: getNodeType
269 });
270 await visitor.crawl();
271 this.indirections.pop();
272 },
273 async crawlPathItemElement(pathItemElement) {
274 // @ts-ignore
275 const reference = await this.toReference(toValue(pathItemElement.$ref));
276 this.indirections.push(pathItemElement);
277 const jsonPointer = uriToPointer(toValue(pathItemElement.$ref));
278
279 // possibly non-semantic fragment
280 let referencedElement = jsonPointerEvaluate(jsonPointer, reference.value.result);
281
282 // applying semantics to a fragment
283 if (isPrimitiveElement(referencedElement)) {
284 referencedElement = PathItemElement.refract(referencedElement);
285 }
286
287 // detect direct or indirect reference
288 if (this.indirections.includes(referencedElement)) {
289 throw new ApiDOMError('Recursive Path Item Object reference detected');
290 }
291
292 // detect maximum depth of dereferencing
293 if (this.indirections.length > this.options.dereference.maxDepth) {
294 throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
295 }
296
297 // dive deep into the fragment
298 const visitor = OpenApi3_1ResolveVisitor({
299 reference,
300 namespace: this.namespace,
301 indirections: [...this.indirections],
302 options: this.options
303 });
304 await visitAsync(referencedElement, visitor, {
305 keyMap,
306 nodeTypeGetter: getNodeType
307 });
308 await visitor.crawl();
309 this.indirections.pop();
310 },
311 async crawlSchemaElement(referencingElement) {
312 // compute baseURI using rules around $id and $ref keywords
313 let reference = await this.toReference(url.unsanitize(this.reference.uri));
314 let {
315 uri: retrievalURI
316 } = reference;
317 const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement);
318 const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
319 const file = File({
320 uri: $refBaseURIStrippedHash
321 });
322 const isUnknownURI = none(r => r.canRead(file), this.options.resolve.resolvers);
323 const isURL = !isUnknownURI;
324 const isExternalURL = uri => url.stripHash(this.reference.uri) !== uri;
325 this.indirections.push(referencingElement);
326
327 // determining reference, proper evaluation and selection mechanism
328 let referencedElement;
329 try {
330 if (isUnknownURI || isURL) {
331 // we're dealing with canonical URI or URL with possible fragment
332 const selector = $refBaseURI;
333 referencedElement = uriEvaluate(selector,
334 // @ts-ignore
335 maybeRefractToSchemaElement(reference.value.result));
336 } else {
337 // we're assuming here that we're dealing with JSON Pointer here
338 retrievalURI = this.toBaseURI(toValue($refBaseURI));
339
340 // ignore resolving external Schema Objects
341 if (!this.options.resolve.external && isExternalURL(retrievalURI)) {
342 // skip traversing this schema element but traverse all it's child elements
343 return undefined;
344 }
345 reference = await this.toReference(url.unsanitize($refBaseURI));
346 const selector = uriToPointer($refBaseURI);
347 referencedElement = maybeRefractToSchemaElement(
348 // @ts-ignore
349 jsonPointerEvaluate(selector, reference.value.result));
350 }
351 } catch (error) {
352 /**
353 * No SchemaElement($id=URL) was not found, so we're going to try to resolve
354 * the URL and assume the returned response is a JSON Schema.
355 */
356 if (isURL && error instanceof EvaluationJsonSchemaUriError) {
357 if (isAnchor(uriToAnchor($refBaseURI))) {
358 // we're dealing with JSON Schema $anchor here
359 retrievalURI = this.toBaseURI(toValue($refBaseURI));
360
361 // ignore resolving external Schema Objects
362 if (!this.options.resolve.external && isExternalURL(retrievalURI)) {
363 // skip traversing this schema element but traverse all it's child elements
364 return undefined;
365 }
366 reference = await this.toReference(url.unsanitize($refBaseURI));
367 const selector = uriToAnchor($refBaseURI);
368 referencedElement = $anchorEvaluate(selector,
369 // @ts-ignore
370 maybeRefractToSchemaElement(reference.value.result));
371 } else {
372 // we're assuming here that we're dealing with JSON Pointer here
373 retrievalURI = this.toBaseURI(toValue($refBaseURI));
374
375 // ignore resolving external Schema Objects
376 if (!this.options.resolve.external && isExternalURL(retrievalURI)) {
377 // skip traversing this schema element but traverse all it's child elements
378 return undefined;
379 }
380 reference = await this.toReference(url.unsanitize($refBaseURI));
381 const selector = uriToPointer($refBaseURI);
382 referencedElement = maybeRefractToSchemaElement(
383 // @ts-ignore
384 jsonPointerEvaluate(selector, reference.value.result));
385 }
386 } else {
387 throw error;
388 }
389 }
390
391 // mark current referencing schema as visited
392 this.visited.add(referencingElement);
393
394 // detect direct or indirect reference
395 if (this.indirections.includes(referencedElement)) {
396 throw new ApiDOMError('Recursive Schema Object reference detected');
397 }
398
399 // detect maximum depth of dereferencing
400 if (this.indirections.length > this.options.dereference.maxDepth) {
401 throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
402 }
403
404 // dive deep into the fragment
405 const visitor = OpenApi3_1ResolveVisitor({
406 reference,
407 namespace: this.namespace,
408 indirections: [...this.indirections],
409 options: this.options,
410 visited: this.visited
411 });
412 await visitAsync(referencedElement, visitor, {
413 keyMap,
414 nodeTypeGetter: getNodeType
415 });
416 await visitor.crawl();
417 this.indirections.pop();
418 return undefined;
419 },
420 async crawl() {
421 /**
422 * Synchronize all parallel resolutions in this place.
423 * After synchronization happened we can be sure that refSet
424 * contains resolved Reference objects.
425 */
426 await pipe(values, allP)(this.crawlingMap);
427 this.crawlingMap = null;
428
429 /* eslint-disable no-await-in-loop */
430 for (const element of this.crawledElements) {
431 if (isReferenceElement(element)) {
432 await this.crawlReferenceElement(element);
433 } else if (isSchemaElement(element)) {
434 await this.crawlSchemaElement(element);
435 } else if (isPathItemElement(element)) {
436 await this.crawlPathItemElement(element);
437 }
438 }
439 /* eslint-enable */
440 }
441 }
442});
443export default OpenApi3_1ResolveVisitor;
Note: See TracBrowser for help on using the repository browser.