fork of hey-api/openapi-ts because I need some additional things
1import { getResolvedInput, sendRequest } from '@hey-api/json-schema-ref-parser';
2import type { MaybeArray } from '@hey-api/types';
3
4import type { Input } from './config/input/types';
5import type { WatchValues } from './types/watch';
6
7const headersEntries = (headers: Headers): Array<[string, string]> => {
8 const entries: Array<[string, string]> = [];
9 headers.forEach((value, key) => {
10 entries.push([key, value]);
11 });
12 return entries;
13};
14
15const mergeHeaders = (
16 ...headers: Array<
17 | RequestInit['headers']
18 | Record<string, MaybeArray<string | number | boolean> | null | undefined | unknown>
19 | undefined
20 >
21): Headers => {
22 const mergedHeaders = new Headers();
23 for (const header of headers) {
24 if (!header) {
25 continue;
26 }
27
28 const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
29
30 for (const [key, value] of iterator) {
31 if (value === null) {
32 mergedHeaders.delete(key);
33 } else if (Array.isArray(value)) {
34 for (const v of value) {
35 mergedHeaders.append(key, v as string);
36 }
37 } else if (value !== undefined) {
38 // assume object headers are meant to be JSON stringified, i.e. their
39 // content value in OpenAPI specification is 'application/json'
40 mergedHeaders.set(
41 key,
42 typeof value === 'object' ? JSON.stringify(value) : (value as string),
43 );
44 }
45 }
46 }
47 return mergedHeaders;
48};
49
50type SpecResponse = {
51 arrayBuffer: ArrayBuffer | undefined;
52 error?: never;
53 resolvedInput: ReturnType<typeof getResolvedInput>;
54 response?: never;
55};
56
57type SpecError = {
58 arrayBuffer?: never;
59 error: 'not-modified' | 'not-ok';
60 resolvedInput?: never;
61 response: Response;
62};
63
64/**
65 * @internal
66 */
67export async function getSpec({
68 fetchOptions,
69 inputPath,
70 timeout,
71 watch,
72}: {
73 fetchOptions?: RequestInit;
74 inputPath: Input['path'];
75 timeout: number | undefined;
76 watch: WatchValues;
77}): Promise<SpecResponse | SpecError> {
78 const resolvedInput = getResolvedInput({ pathOrUrlOrSchema: inputPath });
79
80 let arrayBuffer: ArrayBuffer | undefined;
81 // boolean signals whether the file has **definitely** changed
82 let hasChanged: boolean | undefined;
83 let response: Response | undefined;
84
85 if (resolvedInput.type === 'url') {
86 // do NOT send HEAD request on first run or if unsupported
87 if (watch.lastValue && watch.isHeadMethodSupported !== false) {
88 try {
89 const request = await sendRequest({
90 fetchOptions: {
91 method: 'HEAD',
92 ...fetchOptions,
93 headers: mergeHeaders(fetchOptions?.headers, watch.headers),
94 },
95 timeout,
96 url: resolvedInput.path,
97 });
98
99 if (request.response.status >= 300) {
100 return {
101 error: 'not-ok',
102 response: request.response,
103 };
104 }
105
106 response = request.response;
107 } catch (error) {
108 const message = error instanceof Error ? error.message : String(error);
109 return {
110 error: 'not-ok',
111 response: new Response(message, { status: 500 }),
112 };
113 }
114
115 if (!response.ok && watch.isHeadMethodSupported) {
116 // assume the server is no longer running
117 // do nothing, it might be restarted later
118 return {
119 error: 'not-ok',
120 response,
121 };
122 }
123
124 if (watch.isHeadMethodSupported === undefined) {
125 watch.isHeadMethodSupported = response.ok;
126 }
127
128 if (response.status === 304) {
129 return {
130 error: 'not-modified',
131 response,
132 };
133 }
134
135 if (hasChanged === undefined) {
136 const eTag = response.headers.get('ETag');
137 if (eTag) {
138 hasChanged = eTag !== watch.headers.get('If-None-Match');
139
140 if (hasChanged) {
141 watch.headers.set('If-None-Match', eTag);
142 }
143 }
144 }
145
146 if (hasChanged === undefined) {
147 const lastModified = response.headers.get('Last-Modified');
148 if (lastModified) {
149 hasChanged = lastModified !== watch.headers.get('If-Modified-Since');
150
151 if (hasChanged) {
152 watch.headers.set('If-Modified-Since', lastModified);
153 }
154 }
155 }
156
157 // we definitely know the input has not changed
158 if (hasChanged === false) {
159 return {
160 error: 'not-modified',
161 response,
162 };
163 }
164 }
165
166 try {
167 const request = await sendRequest({
168 fetchOptions: {
169 method: 'GET',
170 ...fetchOptions,
171 },
172 timeout,
173 url: resolvedInput.path,
174 });
175
176 if (request.response.status >= 300) {
177 return {
178 error: 'not-ok',
179 response: request.response,
180 };
181 }
182
183 response = request.response;
184 } catch (error) {
185 const message = error instanceof Error ? error.message : String(error);
186 return {
187 error: 'not-ok',
188 response: new Response(message, { status: 500 }),
189 };
190 }
191
192 if (!response.ok) {
193 // assume the server is no longer running
194 // do nothing, it might be restarted later
195 return {
196 error: 'not-ok',
197 response,
198 };
199 }
200
201 arrayBuffer = response.body ? await response.arrayBuffer() : new ArrayBuffer(0);
202
203 if (hasChanged === undefined) {
204 const content = new TextDecoder().decode(arrayBuffer);
205 hasChanged = content !== watch.lastValue;
206 watch.lastValue = content;
207 }
208 } else {
209 // we do not support watch mode for files or raw spec data
210 if (!watch.lastValue) {
211 watch.lastValue = resolvedInput.type;
212 } else {
213 hasChanged = false;
214 }
215 }
216
217 if (hasChanged === false) {
218 return {
219 error: 'not-modified',
220 response: response!,
221 };
222 }
223
224 return {
225 arrayBuffer,
226 resolvedInput,
227 };
228}