source: node_modules/@swagger-api/apidom-reference/es/resolve/strategies/openapi-3-0/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: 9.5 KB
Line 
1import stampit from 'stampit';
2import { propEq, values, has, pipe } 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, uriToPointer } from '@swagger-api/apidom-json-pointer';
7import { getNodeType, isReferenceElement, isReferenceLikeElement, isPathItemElement, keyMap, ReferenceElement, PathItemElement } from '@swagger-api/apidom-ns-openapi-3-0';
8import MaximumDereferenceDepthError from "../../../errors/MaximumDereferenceDepthError.mjs";
9import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.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// eslint-disable-next-line @typescript-eslint/naming-convention
16const OpenApi3_0ResolveVisitor = stampit({
17 props: {
18 indirections: [],
19 namespace: null,
20 reference: null,
21 crawledElements: null,
22 crawlingMap: null,
23 options: null
24 },
25 init({
26 reference,
27 namespace,
28 indirections = [],
29 options
30 }) {
31 this.indirections = indirections;
32 this.namespace = namespace;
33 this.reference = reference;
34 this.crawledElements = [];
35 this.crawlingMap = {};
36 this.options = options;
37 },
38 methods: {
39 toBaseURI(uri) {
40 return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
41 },
42 async toReference(uri) {
43 // detect maximum depth of resolution
44 if (this.reference.depth >= this.options.resolve.maxDepth) {
45 throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`);
46 }
47 const baseURI = this.toBaseURI(uri);
48 const {
49 refSet
50 } = this.reference;
51
52 // we've already processed this Reference in past
53 if (refSet.has(baseURI)) {
54 return refSet.find(propEq(baseURI, 'uri'));
55 }
56 const parseResult = await parse(url.unsanitize(baseURI), {
57 ...this.options,
58 parse: {
59 ...this.options.parse,
60 mediaType: 'text/plain'
61 }
62 });
63
64 // register new Reference with ReferenceSet
65 const reference = Reference({
66 uri: baseURI,
67 value: parseResult,
68 depth: this.reference.depth + 1
69 });
70 refSet.add(reference);
71 return reference;
72 },
73 ReferenceElement(referenceElement) {
74 const uri = toValue(referenceElement.$ref);
75 const retrievalURI = this.toBaseURI(uri);
76
77 // ignore resolving external Reference Objects
78 if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) {
79 return false;
80 }
81 if (!has(retrievalURI, this.crawlingMap)) {
82 this.crawlingMap[retrievalURI] = this.toReference(uri);
83 }
84 this.crawledElements.push(referenceElement);
85 return undefined;
86 },
87 PathItemElement(pathItemElement) {
88 // ignore PathItemElement without $ref field
89 if (!isStringElement(pathItemElement.$ref)) {
90 return undefined;
91 }
92 const uri = toValue(pathItemElement.$ref);
93 const retrievalURI = this.toBaseURI(uri);
94
95 // ignore resolving external Path Item Objects
96 if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) {
97 return undefined;
98 }
99 if (!has(retrievalURI, this.crawlingMap)) {
100 this.crawlingMap[retrievalURI] = this.toReference(uri);
101 }
102 this.crawledElements.push(pathItemElement);
103 return undefined;
104 },
105 LinkElement(linkElement) {
106 // ignore LinkElement without operationRef or operationId field
107 if (!isStringElement(linkElement.operationRef) && !isStringElement(linkElement.operationId)) {
108 return undefined;
109 }
110
111 // operationRef and operationId are mutually exclusive
112 if (isStringElement(linkElement.operationRef) && isStringElement(linkElement.operationId)) {
113 throw new ApiDOMError('LinkElement operationRef and operationId are mutually exclusive.');
114 }
115 if (isStringElement(linkElement.operationRef)) {
116 const uri = toValue(linkElement.operationRef);
117 const retrievalURI = this.toBaseURI(uri);
118
119 // ignore resolving LinkElement.operationRef
120 if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) {
121 return undefined;
122 }
123 if (!has(retrievalURI, this.crawlingMap)) {
124 this.crawlingMap[retrievalURI] = this.toReference(uri);
125 }
126 }
127 return undefined;
128 },
129 ExampleElement(exampleElement) {
130 // ignore ExampleElement without externalValue field
131 if (!isStringElement(exampleElement.externalValue)) {
132 return undefined;
133 }
134
135 // value and externalValue fields are mutually exclusive
136 if (exampleElement.hasKey('value') && isStringElement(exampleElement.externalValue)) {
137 throw new ApiDOMError('ExampleElement value and externalValue fields are mutually exclusive.');
138 }
139 const uri = toValue(exampleElement.externalValue);
140 const retrievalURI = this.toBaseURI(uri);
141
142 // ignore resolving ExampleElement externalValue
143 if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) {
144 return undefined;
145 }
146 if (!has(retrievalURI, this.crawlingMap)) {
147 this.crawlingMap[retrievalURI] = this.toReference(uri);
148 }
149 return undefined;
150 },
151 async crawlReferenceElement(referenceElement) {
152 // @ts-ignore
153 const reference = await this.toReference(toValue(referenceElement.$ref));
154 this.indirections.push(referenceElement);
155 const jsonPointer = uriToPointer(toValue(referenceElement.$ref));
156
157 // possibly non-semantic fragment
158 let fragment = evaluate(jsonPointer, reference.value.result);
159
160 // applying semantics to a fragment
161 if (isPrimitiveElement(fragment)) {
162 const referencedElementType = toValue(referenceElement.meta.get('referenced-element'));
163 if (isReferenceLikeElement(fragment)) {
164 // handling indirect references
165 fragment = ReferenceElement.refract(fragment);
166 fragment.setMetaProperty('referenced-element', referencedElementType);
167 } else {
168 // handling direct references
169 const ElementClass = this.namespace.getElementClass(referencedElementType);
170 fragment = ElementClass.refract(fragment);
171 }
172 }
173
174 // detect direct or circular reference
175 if (this.indirections.includes(fragment)) {
176 throw new ApiDOMError('Recursive Reference Object detected');
177 }
178
179 // detect maximum depth of dereferencing
180 if (this.indirections.length > this.options.dereference.maxDepth) {
181 throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
182 }
183
184 // dive deep into the fragment
185 const visitor = OpenApi3_0ResolveVisitor({
186 reference,
187 namespace: this.namespace,
188 indirections: [...this.indirections],
189 options: this.options
190 });
191 await visitAsync(fragment, visitor, {
192 keyMap,
193 nodeTypeGetter: getNodeType
194 });
195 await visitor.crawl();
196 this.indirections.pop();
197 },
198 async crawlPathItemElement(pathItemElement) {
199 // @ts-ignore
200 const reference = await this.toReference(toValue(pathItemElement.$ref));
201 this.indirections.push(pathItemElement);
202 const jsonPointer = uriToPointer(toValue(pathItemElement.$ref));
203
204 // possibly non-semantic fragment
205 let referencedElement = evaluate(jsonPointer, reference.value.result);
206
207 // applying semantics to a fragment
208 if (isPrimitiveElement(referencedElement)) {
209 referencedElement = PathItemElement.refract(referencedElement);
210 }
211
212 // detect direct or indirect reference
213 if (this.indirections.includes(referencedElement)) {
214 throw new ApiDOMError('Recursive Path Item Object reference detected');
215 }
216
217 // detect maximum depth of dereferencing
218 if (this.indirections.length > this.options.dereference.maxDepth) {
219 throw new MaximumDereferenceDepthError(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);
220 }
221
222 // dive deep into the fragment
223 const visitor = OpenApi3_0ResolveVisitor({
224 reference,
225 namespace: this.namespace,
226 indirections: [...this.indirections],
227 options: this.options
228 });
229 await visitAsync(referencedElement, visitor, {
230 keyMap,
231 nodeTypeGetter: getNodeType
232 });
233 await visitor.crawl();
234 this.indirections.pop();
235 },
236 async crawl() {
237 /**
238 * Synchronize all parallel resolutions in this place.
239 * After synchronization happened we can be sure that refSet
240 * contains resolved Reference objects.
241 */
242 await pipe(values, allP)(this.crawlingMap);
243 this.crawlingMap = null;
244
245 /* eslint-disable no-await-in-loop */
246 for (const element of this.crawledElements) {
247 if (isReferenceElement(element)) {
248 await this.crawlReferenceElement(element);
249 } else if (isPathItemElement(element)) {
250 await this.crawlPathItemElement(element);
251 }
252 }
253 /* eslint-enabled */
254 }
255 }
256});
257export default OpenApi3_0ResolveVisitor;
Note: See TracBrowser for help on using the repository browser.