fork of hey-api/openapi-ts because I need some additional things
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}