fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 173 lines 5.4 kB view raw
1import type { $RefParser } from '.'; 2import { getResolvedInput } from '.'; 3import type { $RefParserOptions } from './options'; 4import { newFile, parseFile } from './parse'; 5import Pointer from './pointer'; 6import $Ref from './ref'; 7import type $Refs from './refs'; 8import { fileResolver } from './resolvers/file'; 9import { urlResolver } from './resolvers/url'; 10import type { JSONSchema } from './types'; 11import { isHandledError } from './util/errors'; 12import * as url from './util/url'; 13 14/** 15 * Crawls the JSON schema, finds all external JSON references, and resolves their values. 16 * This method does not mutate the JSON schema. The resolved values are added to {@link $RefParser#$refs}. 17 * 18 * NOTE: We only care about EXTERNAL references here. INTERNAL references are only relevant when dereferencing. 19 * 20 * @returns 21 * The promise resolves once all JSON references in the schema have been resolved, 22 * including nested references that are contained in externally-referenced files. 23 */ 24export async function resolveExternal(parser: $RefParser, options: $RefParserOptions) { 25 const promises = crawl(parser.schema, { 26 $refs: parser.$refs, 27 options: options.parse, 28 path: `${parser.$refs._root$Ref.path}#`, 29 }); 30 await Promise.all(promises); 31} 32 33/** 34 * Recursively crawls the given value, and resolves any external JSON references. 35 * 36 * @param obj - The value to crawl. If it's not an object or array, it will be ignored. 37 * @returns An array of promises. There will be one promise for each JSON reference in `obj`. 38 * If `obj` does not contain any JSON references, then the array will be empty. 39 * If any of the JSON references point to files that contain additional JSON references, 40 * then the corresponding promise will internally reference an array of promises. 41 */ 42function crawl<S extends object = JSONSchema>( 43 obj: string | Buffer | S | undefined | null, 44 { 45 $refs, 46 external = false, 47 options, 48 path, 49 seen = new Set(), 50 }: { 51 $refs: $Refs<S>; 52 /** Whether `obj` was found in an external document. */ 53 external?: boolean; 54 options: $RefParserOptions['parse']; 55 /** The full path of `obj`, possibly with a JSON Pointer in the hash. */ 56 path: string; 57 seen?: Set<unknown>; 58 }, 59): ReadonlyArray<Promise<unknown>> { 60 let promises: Array<Promise<unknown>> = []; 61 62 if (obj && typeof obj === 'object' && !ArrayBuffer.isView(obj) && !seen.has(obj)) { 63 seen.add(obj); 64 65 if ($Ref.isExternal$Ref(obj)) { 66 promises.push( 67 resolve$Ref<S>(obj, { 68 $refs, 69 options, 70 path, 71 seen, 72 }), 73 ); 74 } 75 76 for (const [key, value] of Object.entries(obj)) { 77 promises = promises.concat( 78 crawl(value, { 79 $refs, 80 external, 81 options, 82 path: Pointer.join(path, key), 83 seen, 84 }), 85 ); 86 } 87 } 88 89 return promises; 90} 91 92/** 93 * Resolves the given JSON Reference, and then crawls the resulting value. 94 * 95 * @param $ref - The JSON Reference to resolve 96 * @param path - The full path of `$ref`, possibly with a JSON Pointer in the hash 97 * @param $refs 98 * @param options 99 * 100 * @returns 101 * The promise resolves once all JSON references in the object have been resolved, 102 * including nested references that are contained in externally-referenced files. 103 */ 104async function resolve$Ref<S extends object = JSONSchema>( 105 $ref: S, 106 { 107 $refs, 108 options, 109 path, 110 seen, 111 }: { 112 $refs: $Refs<S>; 113 options: $RefParserOptions['parse']; 114 path: string; 115 seen: Set<unknown>; 116 }, 117): Promise<unknown> { 118 const resolvedPath = url.resolve(path, ($ref as JSONSchema).$ref!); 119 const withoutHash = url.stripHash(resolvedPath); 120 121 // If this ref points back to an input source we've already merged, avoid re-importing 122 // by checking if the path (without hash) matches a known source in parser and we can serve it internally later. 123 // We keep normal flow but ensure cache hit if already added. 124 // Do we already have this $ref? 125 const ref = $refs._$refs[withoutHash]; 126 if (ref) { 127 // We've already parsed this $ref, so crawl it to resolve its own externals 128 const promises = crawl(ref.value as S, { 129 $refs, 130 external: true, 131 options, 132 path: `${withoutHash}#`, 133 seen, 134 }); 135 return Promise.all(promises); 136 } 137 138 // Parse the $referenced file/url 139 const file = newFile(resolvedPath); 140 141 // Add a new $Ref for this file, even though we don't have the value yet. 142 // This ensures that we don't simultaneously read & parse the same file multiple times 143 const $refAdded = $refs._add(file.url); 144 145 try { 146 const resolvedInput = getResolvedInput({ pathOrUrlOrSchema: resolvedPath }); 147 148 $refAdded.pathType = resolvedInput.type; 149 150 let promises: ReadonlyArray<Promise<unknown>> = []; 151 152 if (resolvedInput.type !== 'json') { 153 const resolver = resolvedInput.type === 'file' ? fileResolver : urlResolver; 154 await resolver.handler({ file }); 155 const parseResult = await parseFile(file, options); 156 $refAdded.value = parseResult.result; 157 promises = crawl(parseResult.result, { 158 $refs, 159 external: true, 160 options, 161 path: `${withoutHash}#`, 162 seen, 163 }); 164 } 165 166 return Promise.all(promises); 167 } catch (error) { 168 if (isHandledError(error)) { 169 $refAdded.value = error; 170 } 171 throw error; 172 } 173}