-
Notifications
You must be signed in to change notification settings - Fork 14
/
visitor.ts
165 lines (143 loc) · 6.01 KB
/
visitor.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import stampit from 'stampit';
import { propEq } from 'ramda';
import { ApiDOMError } from '@swagger-api/apidom-error';
import {
RefElement,
isElement,
isMemberElement,
isArrayElement,
isObjectElement,
isRefElement,
toValue,
refract,
visit,
} from '@swagger-api/apidom-core';
import { uriToPointer as uriToElementID } from '@swagger-api/apidom-json-pointer';
import { Reference as IReference } from '../../../types';
import MaximumResolveDepthError from '../../../errors/MaximumResolveDepthError';
import * as url from '../../../util/url';
import parse from '../../../parse';
import Reference from '../../../Reference';
import { evaluate } from './selectors/element-id';
// @ts-ignore
const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];
/**
* The following rules apply:
*
* 1. When referencing an element in the local document, the id of the element MAY be used
* 2. When referencing remote elements, an absolute URL or relative URL MAY be used
* 3. When a URL fragment exists in the URL given, it references the element with the matching id in the given document. The URL fragment MAY need to be URL decoded before making a match.
* 4. When a URL fragment does not exist, the URL references the root element
* 5. When path is used, it references the given property of the referenced element
* 6. When path is used in an element that includes the data of the pointer (such as with ref), the referenced path MAY need to be converted to a refract structure in order to be valid
*
* WARNING: this implementation only supports referencing elements in the local document. Points 2-4 are not supported.
*/
const ApiDOMDereferenceVisitor = stampit({
props: {
reference: null,
options: null,
},
init({ reference, options }) {
this.reference = reference;
this.options = options;
},
methods: {
toBaseURI(uri: string): string {
return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
},
async toReference(uri: string): Promise<IReference> {
// detect maximum depth of resolution
if (this.reference.depth >= this.options.resolve.maxDepth) {
throw new MaximumResolveDepthError(
`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`,
);
}
const baseURI = this.toBaseURI(uri);
const { refSet } = this.reference;
// we've already processed this Reference in past
if (refSet.has(baseURI)) {
return refSet.find(propEq(baseURI, 'uri'));
}
const parseResult = await parse(url.unsanitize(baseURI), {
...this.options,
parse: { ...this.options.parse, mediaType: 'text/plain' },
});
// register new Reference with ReferenceSet
const reference = Reference({
uri: baseURI,
value: parseResult,
depth: this.reference.depth + 1,
});
refSet.add(reference);
return reference;
},
async RefElement(refElement: RefElement, key: any, parent: any, path: any, ancestors: any[]) {
const refURI = toValue(refElement);
const refNormalizedURI = url.isURI(refURI) ? refURI : `#${refURI}`;
const retrievalURI = this.toBaseURI(refNormalizedURI);
const isExternal = url.stripHash(this.reference.uri) !== retrievalURI;
// ignore resolving external Ref Objects
if (!this.options.resolve.external && isExternal) {
// skip traversing this ref element
return false;
}
const reference = await this.toReference(refNormalizedURI);
const refBaseURI = url.resolve(retrievalURI, refNormalizedURI);
const elementID = uriToElementID(refBaseURI);
let referencedElement: unknown | Element | undefined = evaluate(
elementID,
reference.value.result,
);
if (!isElement(referencedElement)) {
throw new ApiDOMError(`Referenced element with id="${elementID}" was not found`);
}
if (refElement === referencedElement) {
throw new ApiDOMError('RefElement cannot reference itself');
}
if (isRefElement(referencedElement)) {
throw new ApiDOMError('RefElement cannot reference another RefElement');
}
if (isExternal) {
// dive deep into the fragment
const visitor = ApiDOMDereferenceVisitor({ reference, options: this.options });
referencedElement = await visitAsync(referencedElement, visitor);
}
/**
* When path is used, it references the given property of the referenced element
*/
const referencedElementPath: string = toValue(refElement.path);
if (referencedElementPath !== 'element' && isElement(referencedElement)) {
referencedElement = refract(referencedElement[referencedElementPath]);
}
/**
* Transclusion of a Ref Element SHALL be defined in the if/else block below.
*/
if (isObjectElement(referencedElement) && isObjectElement(ancestors[ancestors.length - 1])) {
/**
* If the Ref Element is held by an Object Element and references an Object Element,
* its content entries SHALL be inserted in place of the Ref Element.
*/
parent.splice(key, 1, ...referencedElement.content);
} else if (isArrayElement(referencedElement) && Array.isArray(parent)) {
/**
* If the Ref Element is held by an Array Element and references an Array Element,
* its content entries SHALL be inserted in place of the Ref Element.
*/
parent.splice(key, 1, ...referencedElement.content);
} else if (isMemberElement(parent)) {
/**
* The Ref Element is substituted by the Element it references.
*/
parent.value = referencedElement; // eslint-disable-line no-param-reassign
} else if (Array.isArray(parent)) {
/**
* The Ref Element is substituted by the Element it references.
*/
parent[key] = referencedElement; // eslint-disable-line no-param-reassign
}
return false;
},
},
});
export default ApiDOMDereferenceVisitor;