fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 262 lines 8.0 kB view raw
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}