fork of hey-api/openapi-ts because I need some additional things
1import { getAuthToken } from './core/auth';
2import type {
3 QuerySerializer,
4 QuerySerializerOptions,
5} from './core/bodySerializer';
6import { jsonBodySerializer } from './core/bodySerializer';
7import {
8 serializeArrayParam,
9 serializeObjectParam,
10 serializePrimitiveParam,
11} from './core/pathSerializer';
12import type { Client, ClientOptions, Config, RequestOptions } from './types';
13
14interface PathSerializer {
15 path: Record<string, unknown>;
16 url: string;
17}
18
19const PATH_PARAM_RE = /\{[^{}]+\}/g;
20
21type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
22type MatrixStyle = 'label' | 'matrix' | 'simple';
23type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
24
25const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
26 let url = _url;
27 const matches = _url.match(PATH_PARAM_RE);
28 if (matches) {
29 for (const match of matches) {
30 let explode = false;
31 let name = match.substring(1, match.length - 1);
32 let style: ArraySeparatorStyle = 'simple';
33
34 if (name.endsWith('*')) {
35 explode = true;
36 name = name.substring(0, name.length - 1);
37 }
38
39 if (name.startsWith('.')) {
40 name = name.substring(1);
41 style = 'label';
42 } else if (name.startsWith(';')) {
43 name = name.substring(1);
44 style = 'matrix';
45 }
46
47 const value = path[name];
48
49 if (value === undefined || value === null) {
50 continue;
51 }
52
53 if (Array.isArray(value)) {
54 url = url.replace(
55 match,
56 serializeArrayParam({ explode, name, style, value }),
57 );
58 continue;
59 }
60
61 if (typeof value === 'object') {
62 url = url.replace(
63 match,
64 serializeObjectParam({
65 explode,
66 name,
67 style,
68 value: value as Record<string, unknown>,
69 valueOnly: true,
70 }),
71 );
72 continue;
73 }
74
75 if (style === 'matrix') {
76 url = url.replace(
77 match,
78 `;${serializePrimitiveParam({
79 name,
80 value: value as string,
81 })}`,
82 );
83 continue;
84 }
85
86 const replaceValue = encodeURIComponent(
87 style === 'label' ? `.${value as string}` : (value as string),
88 );
89 url = url.replace(match, replaceValue);
90 }
91 }
92 return url;
93};
94
95export const createQuerySerializer = <T = unknown>({
96 allowReserved,
97 array,
98 object,
99}: QuerySerializerOptions = {}) => {
100 const querySerializer = (queryParams: T) => {
101 const search: string[] = [];
102 if (queryParams && typeof queryParams === 'object') {
103 for (const name in queryParams) {
104 const value = queryParams[name];
105
106 if (value === undefined || value === null) {
107 continue;
108 }
109
110 if (Array.isArray(value)) {
111 const serializedArray = serializeArrayParam({
112 allowReserved,
113 explode: true,
114 name,
115 style: 'form',
116 value,
117 ...array,
118 });
119 if (serializedArray) search.push(serializedArray);
120 } else if (typeof value === 'object') {
121 const serializedObject = serializeObjectParam({
122 allowReserved,
123 explode: true,
124 name,
125 style: 'deepObject',
126 value: value as Record<string, unknown>,
127 ...object,
128 });
129 if (serializedObject) search.push(serializedObject);
130 } else {
131 const serializedPrimitive = serializePrimitiveParam({
132 allowReserved,
133 name,
134 value: value as string,
135 });
136 if (serializedPrimitive) search.push(serializedPrimitive);
137 }
138 }
139 }
140 return search.join('&');
141 };
142 return querySerializer;
143};
144
145/**
146 * Infers parseAs value from provided Content-Type header.
147 */
148export const getParseAs = (
149 contentType: string | null,
150): Exclude<Config['parseAs'], 'auto'> => {
151 if (!contentType) {
152 // If no Content-Type header is provided, the best we can do is return the raw response body,
153 // which is effectively the same as the 'stream' option.
154 return 'stream';
155 }
156
157 const cleanContent = contentType.split(';')[0]?.trim();
158
159 if (!cleanContent) {
160 return;
161 }
162
163 if (
164 cleanContent.startsWith('application/json') ||
165 cleanContent.endsWith('+json')
166 ) {
167 return 'json';
168 }
169
170 if (cleanContent === 'multipart/form-data') {
171 return 'formData';
172 }
173
174 if (
175 ['application/', 'audio/', 'image/', 'video/'].some((type) =>
176 cleanContent.startsWith(type),
177 )
178 ) {
179 return 'blob';
180 }
181
182 if (cleanContent.startsWith('text/')) {
183 return 'text';
184 }
185
186 return;
187};
188
189const checkForExistence = (
190 options: Pick<RequestOptions, 'auth' | 'query'> & {
191 headers: Headers;
192 },
193 name?: string,
194): boolean => {
195 if (!name) {
196 return false;
197 }
198 if (
199 options.headers.has(name) ||
200 options.query?.[name] ||
201 options.headers.get('Cookie')?.includes(`${name}=`)
202 ) {
203 return true;
204 }
205 return false;
206};
207
208export const setAuthParams = async ({
209 security,
210 ...options
211}: Pick<Required<RequestOptions>, 'security'> &
212 Pick<RequestOptions, 'auth' | 'query'> & {
213 headers: Headers;
214 }) => {
215 for (const auth of security) {
216 if (checkForExistence(options, auth.name)) {
217 continue;
218 }
219
220 const token = await getAuthToken(auth, options.auth);
221
222 if (!token) {
223 continue;
224 }
225
226 const name = auth.name ?? 'Authorization';
227
228 switch (auth.in) {
229 case 'query':
230 if (!options.query) {
231 options.query = {};
232 }
233 options.query[name] = token;
234 break;
235 case 'cookie':
236 options.headers.append('Cookie', `${name}=${token}`);
237 break;
238 case 'header':
239 default:
240 options.headers.set(name, token);
241 break;
242 }
243 }
244};
245
246export const buildUrl: Client['buildUrl'] = (options) => {
247 const url = getUrl({
248 baseUrl: options.baseUrl as string,
249 path: options.path,
250 query: options.query,
251 querySerializer:
252 typeof options.querySerializer === 'function'
253 ? options.querySerializer
254 : createQuerySerializer(options.querySerializer),
255 url: options.url,
256 });
257 return url;
258};
259
260export const getUrl = ({
261 baseUrl,
262 path,
263 query,
264 querySerializer,
265 url: _url,
266}: {
267 baseUrl?: string;
268 path?: Record<string, unknown>;
269 query?: Record<string, unknown>;
270 querySerializer: QuerySerializer;
271 url: string;
272}) => {
273 const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
274 let url = (baseUrl ?? '') + pathUrl;
275 if (path) {
276 url = defaultPathSerializer({ path, url });
277 }
278 let search = query ? querySerializer(query) : '';
279 if (search.startsWith('?')) {
280 search = search.substring(1);
281 }
282 if (search) {
283 url += `?${search}`;
284 }
285 return url;
286};
287
288export const mergeConfigs = (a: Config, b: Config): Config => {
289 const config = { ...a, ...b };
290 if (config.baseUrl?.endsWith('/')) {
291 config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
292 }
293 config.headers = mergeHeaders(a.headers, b.headers);
294 return config;
295};
296
297export const mergeHeaders = (
298 ...headers: Array<Required<Config>['headers'] | undefined>
299): Headers => {
300 const mergedHeaders = new Headers();
301 for (const header of headers) {
302 if (!header || typeof header !== 'object') {
303 continue;
304 }
305
306 const iterator =
307 header instanceof Headers ? header.entries() : Object.entries(header);
308
309 for (const [key, value] of iterator) {
310 if (value === null) {
311 mergedHeaders.delete(key);
312 } else if (Array.isArray(value)) {
313 for (const v of value) {
314 mergedHeaders.append(key, v as string);
315 }
316 } else if (value !== undefined) {
317 // assume object headers are meant to be JSON stringified, i.e. their
318 // content value in OpenAPI specification is 'application/json'
319 mergedHeaders.set(
320 key,
321 typeof value === 'object' ? JSON.stringify(value) : (value as string),
322 );
323 }
324 }
325 }
326 return mergedHeaders;
327};
328
329type ErrInterceptor<Err, Res, Req, Options> = (
330 error: Err,
331 response: Res,
332 request: Req,
333 options: Options,
334) => Err | Promise<Err>;
335
336type ReqInterceptor<Req, Options> = (
337 request: Req,
338 options: Options,
339) => Req | Promise<Req>;
340
341type ResInterceptor<Res, Req, Options> = (
342 response: Res,
343 request: Req,
344 options: Options,
345) => Res | Promise<Res>;
346
347class Interceptors<Interceptor> {
348 fns: Array<Interceptor | null> = [];
349
350 clear(): void {
351 this.fns = [];
352 }
353
354 eject(id: number | Interceptor): void {
355 const index = this.getInterceptorIndex(id);
356 if (this.fns[index]) {
357 this.fns[index] = null;
358 }
359 }
360
361 exists(id: number | Interceptor): boolean {
362 const index = this.getInterceptorIndex(id);
363 return Boolean(this.fns[index]);
364 }
365
366 getInterceptorIndex(id: number | Interceptor): number {
367 if (typeof id === 'number') {
368 return this.fns[id] ? id : -1;
369 }
370 return this.fns.indexOf(id);
371 }
372
373 update(
374 id: number | Interceptor,
375 fn: Interceptor,
376 ): number | Interceptor | false {
377 const index = this.getInterceptorIndex(id);
378 if (this.fns[index]) {
379 this.fns[index] = fn;
380 return id;
381 }
382 return false;
383 }
384
385 use(fn: Interceptor): number {
386 this.fns.push(fn);
387 return this.fns.length - 1;
388 }
389}
390
391export interface Middleware<Req, Res, Err, Options> {
392 error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
393 request: Interceptors<ReqInterceptor<Req, Options>>;
394 response: Interceptors<ResInterceptor<Res, Req, Options>>;
395}
396
397export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
398 Req,
399 Res,
400 Err,
401 Options
402> => ({
403 error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
404 request: new Interceptors<ReqInterceptor<Req, Options>>(),
405 response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
406});
407
408const defaultQuerySerializer = createQuerySerializer({
409 allowReserved: false,
410 array: {
411 explode: true,
412 style: 'form',
413 },
414 object: {
415 explode: true,
416 style: 'deepObject',
417 },
418});
419
420const defaultHeaders = {
421 'Content-Type': 'application/json',
422};
423
424export const createConfig = <T extends ClientOptions = ClientOptions>(
425 override: Config<Omit<ClientOptions, keyof T> & T> = {},
426): Config<Omit<ClientOptions, keyof T> & T> => ({
427 ...jsonBodySerializer,
428 headers: defaultHeaders,
429 parseAs: 'auto',
430 querySerializer: defaultQuerySerializer,
431 ...override,
432});