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