fork of hey-api/openapi-ts because I need some additional things
1import { ono } from '@jsdevtools/ono';
2
3import type { DereferenceOptions, ParserOptions } from './options';
4import Pointer from './pointer';
5import $Ref from './ref';
6import type $Refs from './refs';
7import type { JSONSchema } from './types';
8import { TimeoutError } from './util/errors';
9import * as url from './util/url';
10
11/**
12 * Recursively crawls the given value, and dereferences any JSON references.
13 *
14 * @param obj - The value to crawl. If it's not an object or array, it will be ignored.
15 * @param path - The full path of `obj`, possibly with a JSON Pointer in the hash
16 * @param pathFromRoot - The path of `obj` from the schema root
17 * @param parents - An array of the parent objects that have already been dereferenced
18 * @param processedObjects - An array of all the objects that have already been processed
19 * @param dereferencedCache - An map of all the dereferenced objects
20 * @param $refs
21 * @param options
22 * @param startTime - The time when the dereferencing started
23 * @returns
24 */
25function crawl<S extends object = JSONSchema>(
26 obj: any,
27 path: string,
28 pathFromRoot: string,
29 parents: Set<any>,
30 processedObjects: Set<any>,
31 dereferencedCache: any,
32 $refs: $Refs<S>,
33 options: ParserOptions,
34 startTime: number,
35) {
36 let dereferenced;
37 const result = {
38 circular: false,
39 value: obj,
40 };
41
42 if (options && options.timeoutMs) {
43 if (Date.now() - startTime > options.timeoutMs) {
44 throw new TimeoutError(options.timeoutMs);
45 }
46 }
47 const derefOptions = (options.dereference || {}) as DereferenceOptions;
48 const isExcludedPath = derefOptions.excludedPathMatcher || (() => false);
49
50 if (derefOptions?.circular === 'ignore' || !processedObjects.has(obj)) {
51 if (
52 obj &&
53 typeof obj === 'object' &&
54 !ArrayBuffer.isView(obj) &&
55 !isExcludedPath(pathFromRoot)
56 ) {
57 parents.add(obj);
58 processedObjects.add(obj);
59
60 if ($Ref.isAllowed$Ref(obj)) {
61 dereferenced = dereference$Ref(
62 obj,
63 path,
64 pathFromRoot,
65 parents,
66 processedObjects,
67 dereferencedCache,
68 $refs,
69 options,
70 startTime,
71 );
72 result.circular = dereferenced.circular;
73 result.value = dereferenced.value;
74 } else {
75 for (const key of Object.keys(obj)) {
76 const keyPath = Pointer.join(path, key);
77 const keyPathFromRoot = Pointer.join(pathFromRoot, key);
78
79 if (isExcludedPath(keyPathFromRoot)) {
80 continue;
81 }
82
83 const value = obj[key];
84 let circular = false;
85
86 if ($Ref.isAllowed$Ref(value)) {
87 dereferenced = dereference$Ref(
88 value,
89 keyPath,
90 keyPathFromRoot,
91 parents,
92 processedObjects,
93 dereferencedCache,
94 $refs,
95 options,
96 startTime,
97 );
98 circular = dereferenced.circular;
99 // Avoid pointless mutations; breaks frozen objects to no profit
100 if (obj[key] !== dereferenced.value) {
101 obj[key] = dereferenced.value;
102 derefOptions?.onDereference?.(value.$ref, obj[key], obj, key);
103 }
104 } else {
105 if (!parents.has(value)) {
106 dereferenced = crawl(
107 value,
108 keyPath,
109 keyPathFromRoot,
110 parents,
111 processedObjects,
112 dereferencedCache,
113 $refs,
114 options,
115 startTime,
116 );
117 circular = dereferenced.circular;
118 // Avoid pointless mutations; breaks frozen objects to no profit
119 if (obj[key] !== dereferenced.value) {
120 obj[key] = dereferenced.value;
121 }
122 } else {
123 circular = foundCircularReference(keyPath, $refs, options);
124 }
125 }
126
127 // Set the "isCircular" flag if this or any other property is circular
128 result.circular = result.circular || circular;
129 }
130 }
131
132 parents.delete(obj);
133 }
134 }
135
136 return result;
137}
138
139/**
140 * Dereferences the given JSON Reference, and then crawls the resulting value.
141 *
142 * @param $ref - The JSON Reference to resolve
143 * @param path - The full path of `$ref`, possibly with a JSON Pointer in the hash
144 * @param pathFromRoot - The path of `$ref` from the schema root
145 * @param parents - An array of the parent objects that have already been dereferenced
146 * @param processedObjects - An array of all the objects that have already been dereferenced
147 * @param dereferencedCache - An map of all the dereferenced objects
148 * @param $refs
149 * @param options
150 * @returns
151 */
152function dereference$Ref<S extends object = JSONSchema>(
153 $ref: any,
154 path: string,
155 pathFromRoot: string,
156 parents: Set<any>,
157 processedObjects: any,
158 dereferencedCache: any,
159 $refs: $Refs<S>,
160 options: ParserOptions,
161 startTime: number,
162) {
163 const $refPath = url.resolve(path, $ref.$ref);
164
165 const cache = dereferencedCache.get($refPath);
166 if (cache && !cache.circular) {
167 const refKeys = Object.keys($ref);
168 if (refKeys.length > 1) {
169 const extraKeys = {};
170 for (const key of refKeys) {
171 if (key !== '$ref' && !(key in cache.value)) {
172 // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
173 extraKeys[key] = $ref[key];
174 }
175 }
176 return {
177 circular: cache.circular,
178 value: Object.assign({}, structuredClone(cache.value), extraKeys),
179 };
180 }
181
182 // Return a deep-cloned value so each occurrence is an independent copy
183 return { circular: cache.circular, value: structuredClone(cache.value) };
184 }
185
186 const pointer = $refs._resolve($refPath, path, options);
187
188 if (pointer === null) {
189 return {
190 circular: false,
191 value: null,
192 };
193 }
194
195 // Check for circular references
196 const directCircular = pointer.circular;
197 let circular = directCircular || parents.has(pointer.value);
198 if (circular) {
199 foundCircularReference(path, $refs, options);
200 }
201
202 // Dereference the JSON reference
203 let dereferencedValue = $Ref.dereference($ref, pointer.value);
204
205 // Crawl the dereferenced value (unless it's circular)
206 if (!circular) {
207 // Determine if the dereferenced value is circular
208 const dereferenced = crawl(
209 dereferencedValue,
210 pointer.path,
211 pathFromRoot,
212 parents,
213 processedObjects,
214 dereferencedCache,
215 $refs,
216 options,
217 startTime,
218 );
219 circular = dereferenced.circular;
220 dereferencedValue = dereferenced.value;
221 }
222
223 if (circular && !directCircular && options.dereference?.circular === 'ignore') {
224 // The user has chosen to "ignore" circular references, so don't change the value
225 dereferencedValue = $ref;
226 }
227
228 if (directCircular) {
229 // The pointer is a DIRECT circular reference (i.e. it references itself).
230 // So replace the $ref path with the absolute path from the JSON Schema root
231 dereferencedValue.$ref = pathFromRoot;
232 }
233
234 const dereferencedObject = {
235 circular,
236 value: dereferencedValue,
237 };
238
239 // only cache if no extra properties than $ref
240 if (Object.keys($ref).length === 1) {
241 dereferencedCache.set($refPath, dereferencedObject);
242 }
243
244 return dereferencedObject;
245}
246
247/**
248 * Called when a circular reference is found.
249 * It sets the {@link $Refs#circular} flag, and throws an error if options.dereference.circular is false.
250 *
251 * @param keyPath - The JSON Reference path of the circular reference
252 * @param $refs
253 * @param options
254 * @returns - always returns true, to indicate that a circular reference was found
255 */
256function foundCircularReference(keyPath: any, $refs: any, options: any) {
257 $refs.circular = true;
258 if (!options.dereference.circular) {
259 throw ono.reference(`Circular $ref pointer found at ${keyPath}`);
260 }
261 return true;
262}