-
-
Notifications
You must be signed in to change notification settings - Fork 233
/
Copy pathdereference.ts
315 lines (285 loc) · 9.92 KB
/
dereference.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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
import $Ref from "./ref.js";
import Pointer from "./pointer.js";
import { ono } from "@jsdevtools/ono";
import * as url from "./util/url.js";
import type $Refs from "./refs.js";
import type { DereferenceOptions, ParserOptions } from "./options.js";
import type { JSONSchema } from "./types";
import type $RefParser from "./index";
import { TimeoutError } from "./util/errors";
export default dereference;
/**
* Crawls the JSON schema, finds all JSON references, and dereferences them.
* This method mutates the JSON schema object, replacing JSON references with their resolved value.
*
* @param parser
* @param options
*/
function dereference<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
parser: $RefParser<S, O>,
options: O,
) {
const start = Date.now();
// console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
const dereferenced = crawl<S, O>(
parser.schema,
parser.$refs._root$Ref.path!,
"#",
new Set(),
new Set(),
new Map(),
parser.$refs,
options,
start,
);
parser.$refs.circular = dereferenced.circular;
parser.schema = dereferenced.value;
}
/**
* Recursively crawls the given value, and dereferences any JSON references.
*
* @param obj - The value to crawl. If it's not an object or array, it will be ignored.
* @param path - The full path of `obj`, possibly with a JSON Pointer in the hash
* @param pathFromRoot - The path of `obj` from the schema root
* @param parents - An array of the parent objects that have already been dereferenced
* @param processedObjects - An array of all the objects that have already been processed
* @param dereferencedCache - An map of all the dereferenced objects
* @param $refs
* @param options
* @param startTime - The time when the dereferencing started
* @returns
*/
function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
obj: any,
path: string,
pathFromRoot: string,
parents: Set<any>,
processedObjects: Set<any>,
dereferencedCache: any,
$refs: $Refs<S, O>,
options: O,
startTime: number,
) {
let dereferenced;
const result = {
value: obj,
circular: false,
};
if (options && options.timeoutMs) {
if (Date.now() - startTime > options.timeoutMs) {
throw new TimeoutError(options.timeoutMs);
}
}
const derefOptions = (options.dereference || {}) as DereferenceOptions;
const isExcludedPath = derefOptions.excludedPathMatcher || (() => false);
if (derefOptions?.circular === "ignore" || !processedObjects.has(obj)) {
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
parents.add(obj);
processedObjects.add(obj);
if ($Ref.isAllowed$Ref(obj, options)) {
dereferenced = dereference$Ref(
obj,
path,
pathFromRoot,
parents,
processedObjects,
dereferencedCache,
$refs,
options,
startTime,
);
result.circular = dereferenced.circular;
result.value = dereferenced.value;
} else {
for (const key of Object.keys(obj)) {
const keyPath = Pointer.join(path, key);
const keyPathFromRoot = Pointer.join(pathFromRoot, key);
if (isExcludedPath(keyPathFromRoot)) {
continue;
}
const value = obj[key];
let circular = false;
if ($Ref.isAllowed$Ref(value, options)) {
dereferenced = dereference$Ref(
value,
keyPath,
keyPathFromRoot,
parents,
processedObjects,
dereferencedCache,
$refs,
options,
startTime,
);
circular = dereferenced.circular;
// Avoid pointless mutations; breaks frozen objects to no profit
if (obj[key] !== dereferenced.value) {
// If we have properties we want to preserve from our dereferenced schema then we need
// to copy them over to our new object.
const preserved: Map<string, unknown> = new Map();
if (derefOptions?.preservedProperties) {
if (typeof obj[key] === "object" && !Array.isArray(obj[key])) {
derefOptions?.preservedProperties.forEach((prop) => {
if (prop in obj[key]) {
preserved.set(prop, obj[key][prop]);
}
});
}
}
obj[key] = dereferenced.value;
// If we have data to preserve and our dereferenced object is still an object then
// we need copy back our preserved data into our dereferenced schema.
if (derefOptions?.preservedProperties) {
if (preserved.size && typeof obj[key] === "object" && !Array.isArray(obj[key])) {
preserved.forEach((value, prop) => {
obj[key][prop] = value;
});
}
}
derefOptions?.onDereference?.(value.$ref, obj[key], obj, key);
}
} else {
if (!parents.has(value)) {
dereferenced = crawl(
value,
keyPath,
keyPathFromRoot,
parents,
processedObjects,
dereferencedCache,
$refs,
options,
startTime,
);
circular = dereferenced.circular;
// Avoid pointless mutations; breaks frozen objects to no profit
if (obj[key] !== dereferenced.value) {
obj[key] = dereferenced.value;
}
} else {
circular = foundCircularReference(keyPath, $refs, options);
}
}
// Set the "isCircular" flag if this or any other property is circular
result.circular = result.circular || circular;
}
}
parents.delete(obj);
}
}
return result;
}
/**
* Dereferences the given JSON Reference, and then crawls the resulting value.
*
* @param $ref - The JSON Reference to resolve
* @param path - The full path of `$ref`, possibly with a JSON Pointer in the hash
* @param pathFromRoot - The path of `$ref` from the schema root
* @param parents - An array of the parent objects that have already been dereferenced
* @param processedObjects - An array of all the objects that have already been dereferenced
* @param dereferencedCache - An map of all the dereferenced objects
* @param $refs
* @param options
* @returns
*/
function dereference$Ref<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
$ref: any,
path: string,
pathFromRoot: string,
parents: Set<any>,
processedObjects: any,
dereferencedCache: any,
$refs: $Refs<S, O>,
options: O,
startTime: number,
) {
const isExternalRef = $Ref.isExternal$Ref($ref);
const shouldResolveOnCwd = isExternalRef && options?.dereference?.externalReferenceResolution === "root";
const $refPath = url.resolve(shouldResolveOnCwd ? url.cwd() : path, $ref.$ref);
const cache = dereferencedCache.get($refPath);
if (cache && !cache.circular) {
const refKeys = Object.keys($ref);
if (refKeys.length > 1) {
const extraKeys = {};
for (const key of refKeys) {
if (key !== "$ref" && !(key in cache.value)) {
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
extraKeys[key] = $ref[key];
}
}
return {
circular: cache.circular,
value: Object.assign({}, cache.value, extraKeys),
};
}
return cache;
}
const pointer = $refs._resolve($refPath, path, options);
if (pointer === null) {
return {
circular: false,
value: null,
};
}
// Check for circular references
const directCircular = pointer.circular;
let circular = directCircular || parents.has(pointer.value);
if (circular) {
foundCircularReference(path, $refs, options);
}
// Dereference the JSON reference
let dereferencedValue = $Ref.dereference($ref, pointer.value);
// Crawl the dereferenced value (unless it's circular)
if (!circular) {
// Determine if the dereferenced value is circular
const dereferenced = crawl(
dereferencedValue,
pointer.path,
pathFromRoot,
parents,
processedObjects,
dereferencedCache,
$refs,
options,
startTime,
);
circular = dereferenced.circular;
dereferencedValue = dereferenced.value;
}
if (circular && !directCircular && options.dereference?.circular === "ignore") {
// The user has chosen to "ignore" circular references, so don't change the value
dereferencedValue = $ref;
}
if (directCircular) {
// The pointer is a DIRECT circular reference (i.e. it references itself).
// So replace the $ref path with the absolute path from the JSON Schema root
dereferencedValue.$ref = pathFromRoot;
}
const dereferencedObject = {
circular,
value: dereferencedValue,
};
// only cache if no extra properties than $ref
if (Object.keys($ref).length === 1) {
dereferencedCache.set($refPath, dereferencedObject);
}
return dereferencedObject;
}
/**
* Called when a circular reference is found.
* It sets the {@link $Refs#circular} flag, executes the options.dereference.onCircular callback,
* and throws an error if options.dereference.circular is false.
*
* @param keyPath - The JSON Reference path of the circular reference
* @param $refs
* @param options
* @returns - always returns true, to indicate that a circular reference was found
*/
function foundCircularReference(keyPath: any, $refs: any, options: any) {
$refs.circular = true;
options?.dereference?.onCircular?.(keyPath);
if (!options.dereference.circular) {
throw ono.reference(`Circular $ref pointer found at ${keyPath}`);
}
return true;
}