fork of hey-api/openapi-ts because I need some additional things

Add test cases for recursive/self-referential schemas

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>

+4324
+8
packages/openapi-ts-tests/main/test/3.0.x.test.ts
··· 639 639 }, 640 640 { 641 641 config: createConfig({ 642 + input: 'transformers-recursive.json', 643 + output: 'transformers-recursive', 644 + plugins: ['@hey-api/client-fetch', '@hey-api/transformers'], 645 + }), 646 + description: 'transforms recursive/self-referential schemas', 647 + }, 648 + { 649 + config: createConfig({ 642 650 input: 'dutchie.json', 643 651 output: 'transforms-properties-required-by-default', 644 652 parser: {
+8
packages/openapi-ts-tests/main/test/3.1.x.test.ts
··· 805 805 }, 806 806 { 807 807 config: createConfig({ 808 + input: 'transformers-recursive.json', 809 + output: 'transformers-recursive', 810 + plugins: ['@hey-api/client-fetch', '@hey-api/transformers'], 811 + }), 812 + description: 'transforms recursive/self-referential schemas', 813 + }, 814 + { 815 + config: createConfig({ 808 816 input: 'type-invalid.json', 809 817 output: 'type-invalid', 810 818 }),
+16
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/client.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { type ClientOptions, type Config, createClient, createConfig } from './client'; 4 + import type { ClientOptions as ClientOptions2 } from './types.gen'; 5 + 6 + /** 7 + * The `createClientConfig()` function will be called on client initialization 8 + * and the returned object will become the client's initial configuration. 9 + * 10 + * You may want to initialize your client this way instead of calling 11 + * `setConfig()`. This is useful for example if you're using Next.js 12 + * to ensure your client always has the correct values. 13 + */ 14 + export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>; 15 + 16 + export const client = createClient(createConfig<ClientOptions2>());
+268
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/client/client.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { createSseClient } from '../core/serverSentEvents.gen'; 4 + import type { HttpMethod } from '../core/types.gen'; 5 + import { getValidRequestBody } from '../core/utils.gen'; 6 + import type { 7 + Client, 8 + Config, 9 + RequestOptions, 10 + ResolvedRequestOptions, 11 + } from './types.gen'; 12 + import { 13 + buildUrl, 14 + createConfig, 15 + createInterceptors, 16 + getParseAs, 17 + mergeConfigs, 18 + mergeHeaders, 19 + setAuthParams, 20 + } from './utils.gen'; 21 + 22 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 23 + body?: any; 24 + headers: ReturnType<typeof mergeHeaders>; 25 + }; 26 + 27 + export const createClient = (config: Config = {}): Client => { 28 + let _config = mergeConfigs(createConfig(), config); 29 + 30 + const getConfig = (): Config => ({ ..._config }); 31 + 32 + const setConfig = (config: Config): Config => { 33 + _config = mergeConfigs(_config, config); 34 + return getConfig(); 35 + }; 36 + 37 + const interceptors = createInterceptors< 38 + Request, 39 + Response, 40 + unknown, 41 + ResolvedRequestOptions 42 + >(); 43 + 44 + const beforeRequest = async (options: RequestOptions) => { 45 + const opts = { 46 + ..._config, 47 + ...options, 48 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 49 + headers: mergeHeaders(_config.headers, options.headers), 50 + serializedBody: undefined, 51 + }; 52 + 53 + if (opts.security) { 54 + await setAuthParams({ 55 + ...opts, 56 + security: opts.security, 57 + }); 58 + } 59 + 60 + if (opts.requestValidator) { 61 + await opts.requestValidator(opts); 62 + } 63 + 64 + if (opts.body !== undefined && opts.bodySerializer) { 65 + opts.serializedBody = opts.bodySerializer(opts.body); 66 + } 67 + 68 + // remove Content-Type header if body is empty to avoid sending invalid requests 69 + if (opts.body === undefined || opts.serializedBody === '') { 70 + opts.headers.delete('Content-Type'); 71 + } 72 + 73 + const url = buildUrl(opts); 74 + 75 + return { opts, url }; 76 + }; 77 + 78 + const request: Client['request'] = async (options) => { 79 + // @ts-expect-error 80 + const { opts, url } = await beforeRequest(options); 81 + const requestInit: ReqInit = { 82 + redirect: 'follow', 83 + ...opts, 84 + body: getValidRequestBody(opts), 85 + }; 86 + 87 + let request = new Request(url, requestInit); 88 + 89 + for (const fn of interceptors.request.fns) { 90 + if (fn) { 91 + request = await fn(request, opts); 92 + } 93 + } 94 + 95 + // fetch must be assigned here, otherwise it would throw the error: 96 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 97 + const _fetch = opts.fetch!; 98 + let response = await _fetch(request); 99 + 100 + for (const fn of interceptors.response.fns) { 101 + if (fn) { 102 + response = await fn(response, request, opts); 103 + } 104 + } 105 + 106 + const result = { 107 + request, 108 + response, 109 + }; 110 + 111 + if (response.ok) { 112 + const parseAs = 113 + (opts.parseAs === 'auto' 114 + ? getParseAs(response.headers.get('Content-Type')) 115 + : opts.parseAs) ?? 'json'; 116 + 117 + if ( 118 + response.status === 204 || 119 + response.headers.get('Content-Length') === '0' 120 + ) { 121 + let emptyData: any; 122 + switch (parseAs) { 123 + case 'arrayBuffer': 124 + case 'blob': 125 + case 'text': 126 + emptyData = await response[parseAs](); 127 + break; 128 + case 'formData': 129 + emptyData = new FormData(); 130 + break; 131 + case 'stream': 132 + emptyData = response.body; 133 + break; 134 + case 'json': 135 + default: 136 + emptyData = {}; 137 + break; 138 + } 139 + return opts.responseStyle === 'data' 140 + ? emptyData 141 + : { 142 + data: emptyData, 143 + ...result, 144 + }; 145 + } 146 + 147 + let data: any; 148 + switch (parseAs) { 149 + case 'arrayBuffer': 150 + case 'blob': 151 + case 'formData': 152 + case 'json': 153 + case 'text': 154 + data = await response[parseAs](); 155 + break; 156 + case 'stream': 157 + return opts.responseStyle === 'data' 158 + ? response.body 159 + : { 160 + data: response.body, 161 + ...result, 162 + }; 163 + } 164 + 165 + if (parseAs === 'json') { 166 + if (opts.responseValidator) { 167 + await opts.responseValidator(data); 168 + } 169 + 170 + if (opts.responseTransformer) { 171 + data = await opts.responseTransformer(data); 172 + } 173 + } 174 + 175 + return opts.responseStyle === 'data' 176 + ? data 177 + : { 178 + data, 179 + ...result, 180 + }; 181 + } 182 + 183 + const textError = await response.text(); 184 + let jsonError: unknown; 185 + 186 + try { 187 + jsonError = JSON.parse(textError); 188 + } catch { 189 + // noop 190 + } 191 + 192 + const error = jsonError ?? textError; 193 + let finalError = error; 194 + 195 + for (const fn of interceptors.error.fns) { 196 + if (fn) { 197 + finalError = (await fn(error, response, request, opts)) as string; 198 + } 199 + } 200 + 201 + finalError = finalError || ({} as string); 202 + 203 + if (opts.throwOnError) { 204 + throw finalError; 205 + } 206 + 207 + // TODO: we probably want to return error and improve types 208 + return opts.responseStyle === 'data' 209 + ? undefined 210 + : { 211 + error: finalError, 212 + ...result, 213 + }; 214 + }; 215 + 216 + const makeMethodFn = 217 + (method: Uppercase<HttpMethod>) => (options: RequestOptions) => 218 + request({ ...options, method }); 219 + 220 + const makeSseFn = 221 + (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => { 222 + const { opts, url } = await beforeRequest(options); 223 + return createSseClient({ 224 + ...opts, 225 + body: opts.body as BodyInit | null | undefined, 226 + headers: opts.headers as unknown as Record<string, string>, 227 + method, 228 + onRequest: async (url, init) => { 229 + let request = new Request(url, init); 230 + for (const fn of interceptors.request.fns) { 231 + if (fn) { 232 + request = await fn(request, opts); 233 + } 234 + } 235 + return request; 236 + }, 237 + url, 238 + }); 239 + }; 240 + 241 + return { 242 + buildUrl, 243 + connect: makeMethodFn('CONNECT'), 244 + delete: makeMethodFn('DELETE'), 245 + get: makeMethodFn('GET'), 246 + getConfig, 247 + head: makeMethodFn('HEAD'), 248 + interceptors, 249 + options: makeMethodFn('OPTIONS'), 250 + patch: makeMethodFn('PATCH'), 251 + post: makeMethodFn('POST'), 252 + put: makeMethodFn('PUT'), 253 + request, 254 + setConfig, 255 + sse: { 256 + connect: makeSseFn('CONNECT'), 257 + delete: makeSseFn('DELETE'), 258 + get: makeSseFn('GET'), 259 + head: makeSseFn('HEAD'), 260 + options: makeSseFn('OPTIONS'), 261 + patch: makeSseFn('PATCH'), 262 + post: makeSseFn('POST'), 263 + put: makeSseFn('PUT'), 264 + trace: makeSseFn('TRACE'), 265 + }, 266 + trace: makeMethodFn('TRACE'), 267 + } as Client; 268 + };
+26
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/client/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { Auth } from '../core/auth.gen'; 4 + export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; 5 + export { 6 + formDataBodySerializer, 7 + jsonBodySerializer, 8 + urlSearchParamsBodySerializer, 9 + } from '../core/bodySerializer.gen'; 10 + export { buildClientParams } from '../core/params.gen'; 11 + export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; 12 + export { createClient } from './client.gen'; 13 + export type { 14 + Client, 15 + ClientOptions, 16 + Config, 17 + CreateClientConfig, 18 + Options, 19 + OptionsLegacyParser, 20 + RequestOptions, 21 + RequestResult, 22 + ResolvedRequestOptions, 23 + ResponseStyle, 24 + TDataShape, 25 + } from './types.gen'; 26 + export { createConfig, mergeHeaders } from './utils.gen';
+268
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/client/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Auth } from '../core/auth.gen'; 4 + import type { 5 + ServerSentEventsOptions, 6 + ServerSentEventsResult, 7 + } from '../core/serverSentEvents.gen'; 8 + import type { 9 + Client as CoreClient, 10 + Config as CoreConfig, 11 + } from '../core/types.gen'; 12 + import type { Middleware } from './utils.gen'; 13 + 14 + export type ResponseStyle = 'data' | 'fields'; 15 + 16 + export interface Config<T extends ClientOptions = ClientOptions> 17 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 18 + CoreConfig { 19 + /** 20 + * Base URL for all requests made by this client. 21 + */ 22 + baseUrl?: T['baseUrl']; 23 + /** 24 + * Fetch API implementation. You can use this option to provide a custom 25 + * fetch instance. 26 + * 27 + * @default globalThis.fetch 28 + */ 29 + fetch?: typeof fetch; 30 + /** 31 + * Please don't use the Fetch client for Next.js applications. The `next` 32 + * options won't have any effect. 33 + * 34 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 35 + */ 36 + next?: never; 37 + /** 38 + * Return the response data parsed in a specified format. By default, `auto` 39 + * will infer the appropriate method from the `Content-Type` response header. 40 + * You can override this behavior with any of the {@link Body} methods. 41 + * Select `stream` if you don't want to parse response data at all. 42 + * 43 + * @default 'auto' 44 + */ 45 + parseAs?: 46 + | 'arrayBuffer' 47 + | 'auto' 48 + | 'blob' 49 + | 'formData' 50 + | 'json' 51 + | 'stream' 52 + | 'text'; 53 + /** 54 + * Should we return only data or multiple fields (data, error, response, etc.)? 55 + * 56 + * @default 'fields' 57 + */ 58 + responseStyle?: ResponseStyle; 59 + /** 60 + * Throw an error instead of returning it in the response? 61 + * 62 + * @default false 63 + */ 64 + throwOnError?: T['throwOnError']; 65 + } 66 + 67 + export interface RequestOptions< 68 + TData = unknown, 69 + TResponseStyle extends ResponseStyle = 'fields', 70 + ThrowOnError extends boolean = boolean, 71 + Url extends string = string, 72 + > extends Config<{ 73 + responseStyle: TResponseStyle; 74 + throwOnError: ThrowOnError; 75 + }>, 76 + Pick< 77 + ServerSentEventsOptions<TData>, 78 + | 'onSseError' 79 + | 'onSseEvent' 80 + | 'sseDefaultRetryDelay' 81 + | 'sseMaxRetryAttempts' 82 + | 'sseMaxRetryDelay' 83 + > { 84 + /** 85 + * Any body that you want to add to your request. 86 + * 87 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 88 + */ 89 + body?: unknown; 90 + path?: Record<string, unknown>; 91 + query?: Record<string, unknown>; 92 + /** 93 + * Security mechanism(s) to use for the request. 94 + */ 95 + security?: ReadonlyArray<Auth>; 96 + url: Url; 97 + } 98 + 99 + export interface ResolvedRequestOptions< 100 + TResponseStyle extends ResponseStyle = 'fields', 101 + ThrowOnError extends boolean = boolean, 102 + Url extends string = string, 103 + > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> { 104 + serializedBody?: string; 105 + } 106 + 107 + export type RequestResult< 108 + TData = unknown, 109 + TError = unknown, 110 + ThrowOnError extends boolean = boolean, 111 + TResponseStyle extends ResponseStyle = 'fields', 112 + > = ThrowOnError extends true 113 + ? Promise< 114 + TResponseStyle extends 'data' 115 + ? TData extends Record<string, unknown> 116 + ? TData[keyof TData] 117 + : TData 118 + : { 119 + data: TData extends Record<string, unknown> 120 + ? TData[keyof TData] 121 + : TData; 122 + request: Request; 123 + response: Response; 124 + } 125 + > 126 + : Promise< 127 + TResponseStyle extends 'data' 128 + ? 129 + | (TData extends Record<string, unknown> 130 + ? TData[keyof TData] 131 + : TData) 132 + | undefined 133 + : ( 134 + | { 135 + data: TData extends Record<string, unknown> 136 + ? TData[keyof TData] 137 + : TData; 138 + error: undefined; 139 + } 140 + | { 141 + data: undefined; 142 + error: TError extends Record<string, unknown> 143 + ? TError[keyof TError] 144 + : TError; 145 + } 146 + ) & { 147 + request: Request; 148 + response: Response; 149 + } 150 + >; 151 + 152 + export interface ClientOptions { 153 + baseUrl?: string; 154 + responseStyle?: ResponseStyle; 155 + throwOnError?: boolean; 156 + } 157 + 158 + type MethodFn = < 159 + TData = unknown, 160 + TError = unknown, 161 + ThrowOnError extends boolean = false, 162 + TResponseStyle extends ResponseStyle = 'fields', 163 + >( 164 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 165 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 166 + 167 + type SseFn = < 168 + TData = unknown, 169 + TError = unknown, 170 + ThrowOnError extends boolean = false, 171 + TResponseStyle extends ResponseStyle = 'fields', 172 + >( 173 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 174 + ) => Promise<ServerSentEventsResult<TData, TError>>; 175 + 176 + type RequestFn = < 177 + TData = unknown, 178 + TError = unknown, 179 + ThrowOnError extends boolean = false, 180 + TResponseStyle extends ResponseStyle = 'fields', 181 + >( 182 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> & 183 + Pick< 184 + Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 185 + 'method' 186 + >, 187 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 188 + 189 + type BuildUrlFn = < 190 + TData extends { 191 + body?: unknown; 192 + path?: Record<string, unknown>; 193 + query?: Record<string, unknown>; 194 + url: string; 195 + }, 196 + >( 197 + options: Pick<TData, 'url'> & Options<TData>, 198 + ) => string; 199 + 200 + export type Client = CoreClient< 201 + RequestFn, 202 + Config, 203 + MethodFn, 204 + BuildUrlFn, 205 + SseFn 206 + > & { 207 + interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>; 208 + }; 209 + 210 + /** 211 + * The `createClientConfig()` function will be called on client initialization 212 + * and the returned object will become the client's initial configuration. 213 + * 214 + * You may want to initialize your client this way instead of calling 215 + * `setConfig()`. This is useful for example if you're using Next.js 216 + * to ensure your client always has the correct values. 217 + */ 218 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 219 + override?: Config<ClientOptions & T>, 220 + ) => Config<Required<ClientOptions> & T>; 221 + 222 + export interface TDataShape { 223 + body?: unknown; 224 + headers?: unknown; 225 + path?: unknown; 226 + query?: unknown; 227 + url: string; 228 + } 229 + 230 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 231 + 232 + export type Options< 233 + TData extends TDataShape = TDataShape, 234 + ThrowOnError extends boolean = boolean, 235 + TResponse = unknown, 236 + TResponseStyle extends ResponseStyle = 'fields', 237 + > = OmitKeys< 238 + RequestOptions<TResponse, TResponseStyle, ThrowOnError>, 239 + 'body' | 'path' | 'query' | 'url' 240 + > & 241 + Omit<TData, 'url'>; 242 + 243 + export type OptionsLegacyParser< 244 + TData = unknown, 245 + ThrowOnError extends boolean = boolean, 246 + TResponseStyle extends ResponseStyle = 'fields', 247 + > = TData extends { body?: any } 248 + ? TData extends { headers?: any } 249 + ? OmitKeys< 250 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 251 + 'body' | 'headers' | 'url' 252 + > & 253 + TData 254 + : OmitKeys< 255 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 256 + 'body' | 'url' 257 + > & 258 + TData & 259 + Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'headers'> 260 + : TData extends { headers?: any } 261 + ? OmitKeys< 262 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 263 + 'headers' | 'url' 264 + > & 265 + TData & 266 + Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'body'> 267 + : OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'url'> & 268 + TData;
+332
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/client/utils.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { getAuthToken } from '../core/auth.gen'; 4 + import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; 5 + import { jsonBodySerializer } from '../core/bodySerializer.gen'; 6 + import { 7 + serializeArrayParam, 8 + serializeObjectParam, 9 + serializePrimitiveParam, 10 + } from '../core/pathSerializer.gen'; 11 + import { getUrl } from '../core/utils.gen'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; 13 + 14 + export const createQuerySerializer = <T = unknown>({ 15 + parameters = {}, 16 + ...args 17 + }: QuerySerializerOptions = {}) => { 18 + const querySerializer = (queryParams: T) => { 19 + const search: string[] = []; 20 + if (queryParams && typeof queryParams === 'object') { 21 + for (const name in queryParams) { 22 + const value = queryParams[name]; 23 + 24 + if (value === undefined || value === null) { 25 + continue; 26 + } 27 + 28 + const options = parameters[name] || args; 29 + 30 + if (Array.isArray(value)) { 31 + const serializedArray = serializeArrayParam({ 32 + allowReserved: options.allowReserved, 33 + explode: true, 34 + name, 35 + style: 'form', 36 + value, 37 + ...options.array, 38 + }); 39 + if (serializedArray) search.push(serializedArray); 40 + } else if (typeof value === 'object') { 41 + const serializedObject = serializeObjectParam({ 42 + allowReserved: options.allowReserved, 43 + explode: true, 44 + name, 45 + style: 'deepObject', 46 + value: value as Record<string, unknown>, 47 + ...options.object, 48 + }); 49 + if (serializedObject) search.push(serializedObject); 50 + } else { 51 + const serializedPrimitive = serializePrimitiveParam({ 52 + allowReserved: options.allowReserved, 53 + name, 54 + value: value as string, 55 + }); 56 + if (serializedPrimitive) search.push(serializedPrimitive); 57 + } 58 + } 59 + } 60 + return search.join('&'); 61 + }; 62 + return querySerializer; 63 + }; 64 + 65 + /** 66 + * Infers parseAs value from provided Content-Type header. 67 + */ 68 + export const getParseAs = ( 69 + contentType: string | null, 70 + ): Exclude<Config['parseAs'], 'auto'> => { 71 + if (!contentType) { 72 + // If no Content-Type header is provided, the best we can do is return the raw response body, 73 + // which is effectively the same as the 'stream' option. 74 + return 'stream'; 75 + } 76 + 77 + const cleanContent = contentType.split(';')[0]?.trim(); 78 + 79 + if (!cleanContent) { 80 + return; 81 + } 82 + 83 + if ( 84 + cleanContent.startsWith('application/json') || 85 + cleanContent.endsWith('+json') 86 + ) { 87 + return 'json'; 88 + } 89 + 90 + if (cleanContent === 'multipart/form-data') { 91 + return 'formData'; 92 + } 93 + 94 + if ( 95 + ['application/', 'audio/', 'image/', 'video/'].some((type) => 96 + cleanContent.startsWith(type), 97 + ) 98 + ) { 99 + return 'blob'; 100 + } 101 + 102 + if (cleanContent.startsWith('text/')) { 103 + return 'text'; 104 + } 105 + 106 + return; 107 + }; 108 + 109 + const checkForExistence = ( 110 + options: Pick<RequestOptions, 'auth' | 'query'> & { 111 + headers: Headers; 112 + }, 113 + name?: string, 114 + ): boolean => { 115 + if (!name) { 116 + return false; 117 + } 118 + if ( 119 + options.headers.has(name) || 120 + options.query?.[name] || 121 + options.headers.get('Cookie')?.includes(`${name}=`) 122 + ) { 123 + return true; 124 + } 125 + return false; 126 + }; 127 + 128 + export const setAuthParams = async ({ 129 + security, 130 + ...options 131 + }: Pick<Required<RequestOptions>, 'security'> & 132 + Pick<RequestOptions, 'auth' | 'query'> & { 133 + headers: Headers; 134 + }) => { 135 + for (const auth of security) { 136 + if (checkForExistence(options, auth.name)) { 137 + continue; 138 + } 139 + 140 + const token = await getAuthToken(auth, options.auth); 141 + 142 + if (!token) { 143 + continue; 144 + } 145 + 146 + const name = auth.name ?? 'Authorization'; 147 + 148 + switch (auth.in) { 149 + case 'query': 150 + if (!options.query) { 151 + options.query = {}; 152 + } 153 + options.query[name] = token; 154 + break; 155 + case 'cookie': 156 + options.headers.append('Cookie', `${name}=${token}`); 157 + break; 158 + case 'header': 159 + default: 160 + options.headers.set(name, token); 161 + break; 162 + } 163 + } 164 + }; 165 + 166 + export const buildUrl: Client['buildUrl'] = (options) => 167 + getUrl({ 168 + baseUrl: options.baseUrl as string, 169 + path: options.path, 170 + query: options.query, 171 + querySerializer: 172 + typeof options.querySerializer === 'function' 173 + ? options.querySerializer 174 + : createQuerySerializer(options.querySerializer), 175 + url: options.url, 176 + }); 177 + 178 + export const mergeConfigs = (a: Config, b: Config): Config => { 179 + const config = { ...a, ...b }; 180 + if (config.baseUrl?.endsWith('/')) { 181 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 182 + } 183 + config.headers = mergeHeaders(a.headers, b.headers); 184 + return config; 185 + }; 186 + 187 + const headersEntries = (headers: Headers): Array<[string, string]> => { 188 + const entries: Array<[string, string]> = []; 189 + headers.forEach((value, key) => { 190 + entries.push([key, value]); 191 + }); 192 + return entries; 193 + }; 194 + 195 + export const mergeHeaders = ( 196 + ...headers: Array<Required<Config>['headers'] | undefined> 197 + ): Headers => { 198 + const mergedHeaders = new Headers(); 199 + for (const header of headers) { 200 + if (!header) { 201 + continue; 202 + } 203 + 204 + const iterator = 205 + header instanceof Headers 206 + ? headersEntries(header) 207 + : Object.entries(header); 208 + 209 + for (const [key, value] of iterator) { 210 + if (value === null) { 211 + mergedHeaders.delete(key); 212 + } else if (Array.isArray(value)) { 213 + for (const v of value) { 214 + mergedHeaders.append(key, v as string); 215 + } 216 + } else if (value !== undefined) { 217 + // assume object headers are meant to be JSON stringified, i.e. their 218 + // content value in OpenAPI specification is 'application/json' 219 + mergedHeaders.set( 220 + key, 221 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 222 + ); 223 + } 224 + } 225 + } 226 + return mergedHeaders; 227 + }; 228 + 229 + type ErrInterceptor<Err, Res, Req, Options> = ( 230 + error: Err, 231 + response: Res, 232 + request: Req, 233 + options: Options, 234 + ) => Err | Promise<Err>; 235 + 236 + type ReqInterceptor<Req, Options> = ( 237 + request: Req, 238 + options: Options, 239 + ) => Req | Promise<Req>; 240 + 241 + type ResInterceptor<Res, Req, Options> = ( 242 + response: Res, 243 + request: Req, 244 + options: Options, 245 + ) => Res | Promise<Res>; 246 + 247 + class Interceptors<Interceptor> { 248 + fns: Array<Interceptor | null> = []; 249 + 250 + clear(): void { 251 + this.fns = []; 252 + } 253 + 254 + eject(id: number | Interceptor): void { 255 + const index = this.getInterceptorIndex(id); 256 + if (this.fns[index]) { 257 + this.fns[index] = null; 258 + } 259 + } 260 + 261 + exists(id: number | Interceptor): boolean { 262 + const index = this.getInterceptorIndex(id); 263 + return Boolean(this.fns[index]); 264 + } 265 + 266 + getInterceptorIndex(id: number | Interceptor): number { 267 + if (typeof id === 'number') { 268 + return this.fns[id] ? id : -1; 269 + } 270 + return this.fns.indexOf(id); 271 + } 272 + 273 + update( 274 + id: number | Interceptor, 275 + fn: Interceptor, 276 + ): number | Interceptor | false { 277 + const index = this.getInterceptorIndex(id); 278 + if (this.fns[index]) { 279 + this.fns[index] = fn; 280 + return id; 281 + } 282 + return false; 283 + } 284 + 285 + use(fn: Interceptor): number { 286 + this.fns.push(fn); 287 + return this.fns.length - 1; 288 + } 289 + } 290 + 291 + export interface Middleware<Req, Res, Err, Options> { 292 + error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>; 293 + request: Interceptors<ReqInterceptor<Req, Options>>; 294 + response: Interceptors<ResInterceptor<Res, Req, Options>>; 295 + } 296 + 297 + export const createInterceptors = <Req, Res, Err, Options>(): Middleware< 298 + Req, 299 + Res, 300 + Err, 301 + Options 302 + > => ({ 303 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 304 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 305 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 306 + }); 307 + 308 + const defaultQuerySerializer = createQuerySerializer({ 309 + allowReserved: false, 310 + array: { 311 + explode: true, 312 + style: 'form', 313 + }, 314 + object: { 315 + explode: true, 316 + style: 'deepObject', 317 + }, 318 + }); 319 + 320 + const defaultHeaders = { 321 + 'Content-Type': 'application/json', 322 + }; 323 + 324 + export const createConfig = <T extends ClientOptions = ClientOptions>( 325 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 326 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 327 + ...jsonBodySerializer, 328 + headers: defaultHeaders, 329 + parseAs: 'auto', 330 + querySerializer: defaultQuerySerializer, 331 + ...override, 332 + });
+42
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/core/auth.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type AuthToken = string | undefined; 4 + 5 + export interface Auth { 6 + /** 7 + * Which part of the request do we use to send the auth? 8 + * 9 + * @default 'header' 10 + */ 11 + in?: 'header' | 'query' | 'cookie'; 12 + /** 13 + * Header or query parameter name. 14 + * 15 + * @default 'Authorization' 16 + */ 17 + name?: string; 18 + scheme?: 'basic' | 'bearer'; 19 + type: 'apiKey' | 'http'; 20 + } 21 + 22 + export const getAuthToken = async ( 23 + auth: Auth, 24 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 25 + ): Promise<string | undefined> => { 26 + const token = 27 + typeof callback === 'function' ? await callback(auth) : callback; 28 + 29 + if (!token) { 30 + return; 31 + } 32 + 33 + if (auth.scheme === 'bearer') { 34 + return `Bearer ${token}`; 35 + } 36 + 37 + if (auth.scheme === 'basic') { 38 + return `Basic ${btoa(token)}`; 39 + } 40 + 41 + return token; 42 + };
+100
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/core/bodySerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { 4 + ArrayStyle, 5 + ObjectStyle, 6 + SerializerOptions, 7 + } from './pathSerializer.gen'; 8 + 9 + export type QuerySerializer = (query: Record<string, unknown>) => string; 10 + 11 + export type BodySerializer = (body: any) => any; 12 + 13 + type QuerySerializerOptionsObject = { 14 + allowReserved?: boolean; 15 + array?: Partial<SerializerOptions<ArrayStyle>>; 16 + object?: Partial<SerializerOptions<ObjectStyle>>; 17 + }; 18 + 19 + export type QuerySerializerOptions = QuerySerializerOptionsObject & { 20 + /** 21 + * Per-parameter serialization overrides. When provided, these settings 22 + * override the global array/object settings for specific parameter names. 23 + */ 24 + parameters?: Record<string, QuerySerializerOptionsObject>; 25 + }; 26 + 27 + const serializeFormDataPair = ( 28 + data: FormData, 29 + key: string, 30 + value: unknown, 31 + ): void => { 32 + if (typeof value === 'string' || value instanceof Blob) { 33 + data.append(key, value); 34 + } else if (value instanceof Date) { 35 + data.append(key, value.toISOString()); 36 + } else { 37 + data.append(key, JSON.stringify(value)); 38 + } 39 + }; 40 + 41 + const serializeUrlSearchParamsPair = ( 42 + data: URLSearchParams, 43 + key: string, 44 + value: unknown, 45 + ): void => { 46 + if (typeof value === 'string') { 47 + data.append(key, value); 48 + } else { 49 + data.append(key, JSON.stringify(value)); 50 + } 51 + }; 52 + 53 + export const formDataBodySerializer = { 54 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 55 + body: T, 56 + ): FormData => { 57 + const data = new FormData(); 58 + 59 + Object.entries(body).forEach(([key, value]) => { 60 + if (value === undefined || value === null) { 61 + return; 62 + } 63 + if (Array.isArray(value)) { 64 + value.forEach((v) => serializeFormDataPair(data, key, v)); 65 + } else { 66 + serializeFormDataPair(data, key, value); 67 + } 68 + }); 69 + 70 + return data; 71 + }, 72 + }; 73 + 74 + export const jsonBodySerializer = { 75 + bodySerializer: <T>(body: T): string => 76 + JSON.stringify(body, (_key, value) => 77 + typeof value === 'bigint' ? value.toString() : value, 78 + ), 79 + }; 80 + 81 + export const urlSearchParamsBodySerializer = { 82 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 83 + body: T, 84 + ): string => { 85 + const data = new URLSearchParams(); 86 + 87 + Object.entries(body).forEach(([key, value]) => { 88 + if (value === undefined || value === null) { 89 + return; 90 + } 91 + if (Array.isArray(value)) { 92 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 93 + } else { 94 + serializeUrlSearchParamsPair(data, key, value); 95 + } 96 + }); 97 + 98 + return data.toString(); 99 + }, 100 + };
+153
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/core/params.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + type Slot = 'body' | 'headers' | 'path' | 'query'; 4 + 5 + export type Field = 6 + | { 7 + in: Exclude<Slot, 'body'>; 8 + /** 9 + * Field name. This is the name we want the user to see and use. 10 + */ 11 + key: string; 12 + /** 13 + * Field mapped name. This is the name we want to use in the request. 14 + * If omitted, we use the same value as `key`. 15 + */ 16 + map?: string; 17 + } 18 + | { 19 + in: Extract<Slot, 'body'>; 20 + /** 21 + * Key isn't required for bodies. 22 + */ 23 + key?: string; 24 + map?: string; 25 + }; 26 + 27 + export interface Fields { 28 + allowExtra?: Partial<Record<Slot, boolean>>; 29 + args?: ReadonlyArray<Field>; 30 + } 31 + 32 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 33 + 34 + const extraPrefixesMap: Record<string, Slot> = { 35 + $body_: 'body', 36 + $headers_: 'headers', 37 + $path_: 'path', 38 + $query_: 'query', 39 + }; 40 + const extraPrefixes = Object.entries(extraPrefixesMap); 41 + 42 + type KeyMap = Map< 43 + string, 44 + { 45 + in: Slot; 46 + map?: string; 47 + } 48 + >; 49 + 50 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 51 + if (!map) { 52 + map = new Map(); 53 + } 54 + 55 + for (const config of fields) { 56 + if ('in' in config) { 57 + if (config.key) { 58 + map.set(config.key, { 59 + in: config.in, 60 + map: config.map, 61 + }); 62 + } 63 + } else if (config.args) { 64 + buildKeyMap(config.args, map); 65 + } 66 + } 67 + 68 + return map; 69 + }; 70 + 71 + interface Params { 72 + body: unknown; 73 + headers: Record<string, unknown>; 74 + path: Record<string, unknown>; 75 + query: Record<string, unknown>; 76 + } 77 + 78 + const stripEmptySlots = (params: Params) => { 79 + for (const [slot, value] of Object.entries(params)) { 80 + if (value && typeof value === 'object' && !Object.keys(value).length) { 81 + delete params[slot as Slot]; 82 + } 83 + } 84 + }; 85 + 86 + export const buildClientParams = ( 87 + args: ReadonlyArray<unknown>, 88 + fields: FieldsConfig, 89 + ) => { 90 + const params: Params = { 91 + body: {}, 92 + headers: {}, 93 + path: {}, 94 + query: {}, 95 + }; 96 + 97 + const map = buildKeyMap(fields); 98 + 99 + let config: FieldsConfig[number] | undefined; 100 + 101 + for (const [index, arg] of args.entries()) { 102 + if (fields[index]) { 103 + config = fields[index]; 104 + } 105 + 106 + if (!config) { 107 + continue; 108 + } 109 + 110 + if ('in' in config) { 111 + if (config.key) { 112 + const field = map.get(config.key)!; 113 + const name = field.map || config.key; 114 + (params[field.in] as Record<string, unknown>)[name] = arg; 115 + } else { 116 + params.body = arg; 117 + } 118 + } else { 119 + for (const [key, value] of Object.entries(arg ?? {})) { 120 + const field = map.get(key); 121 + 122 + if (field) { 123 + const name = field.map || key; 124 + (params[field.in] as Record<string, unknown>)[name] = value; 125 + } else { 126 + const extra = extraPrefixes.find(([prefix]) => 127 + key.startsWith(prefix), 128 + ); 129 + 130 + if (extra) { 131 + const [prefix, slot] = extra; 132 + (params[slot] as Record<string, unknown>)[ 133 + key.slice(prefix.length) 134 + ] = value; 135 + } else { 136 + for (const [slot, allowed] of Object.entries( 137 + config.allowExtra ?? {}, 138 + )) { 139 + if (allowed) { 140 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 141 + break; 142 + } 143 + } 144 + } 145 + } 146 + } 147 + } 148 + } 149 + 150 + stripEmptySlots(params); 151 + 152 + return params; 153 + };
+181
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/core/pathSerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + interface SerializeOptions<T> 4 + extends SerializePrimitiveOptions, 5 + SerializerOptions<T> {} 6 + 7 + interface SerializePrimitiveOptions { 8 + allowReserved?: boolean; 9 + name: string; 10 + } 11 + 12 + export interface SerializerOptions<T> { 13 + /** 14 + * @default true 15 + */ 16 + explode: boolean; 17 + style: T; 18 + } 19 + 20 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 21 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 22 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 23 + export type ObjectStyle = 'form' | 'deepObject'; 24 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 25 + 26 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 27 + value: string; 28 + } 29 + 30 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 31 + switch (style) { 32 + case 'label': 33 + return '.'; 34 + case 'matrix': 35 + return ';'; 36 + case 'simple': 37 + return ','; 38 + default: 39 + return '&'; 40 + } 41 + }; 42 + 43 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 44 + switch (style) { 45 + case 'form': 46 + return ','; 47 + case 'pipeDelimited': 48 + return '|'; 49 + case 'spaceDelimited': 50 + return '%20'; 51 + default: 52 + return ','; 53 + } 54 + }; 55 + 56 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 57 + switch (style) { 58 + case 'label': 59 + return '.'; 60 + case 'matrix': 61 + return ';'; 62 + case 'simple': 63 + return ','; 64 + default: 65 + return '&'; 66 + } 67 + }; 68 + 69 + export const serializeArrayParam = ({ 70 + allowReserved, 71 + explode, 72 + name, 73 + style, 74 + value, 75 + }: SerializeOptions<ArraySeparatorStyle> & { 76 + value: unknown[]; 77 + }) => { 78 + if (!explode) { 79 + const joinedValues = ( 80 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 81 + ).join(separatorArrayNoExplode(style)); 82 + switch (style) { 83 + case 'label': 84 + return `.${joinedValues}`; 85 + case 'matrix': 86 + return `;${name}=${joinedValues}`; 87 + case 'simple': 88 + return joinedValues; 89 + default: 90 + return `${name}=${joinedValues}`; 91 + } 92 + } 93 + 94 + const separator = separatorArrayExplode(style); 95 + const joinedValues = value 96 + .map((v) => { 97 + if (style === 'label' || style === 'simple') { 98 + return allowReserved ? v : encodeURIComponent(v as string); 99 + } 100 + 101 + return serializePrimitiveParam({ 102 + allowReserved, 103 + name, 104 + value: v as string, 105 + }); 106 + }) 107 + .join(separator); 108 + return style === 'label' || style === 'matrix' 109 + ? separator + joinedValues 110 + : joinedValues; 111 + }; 112 + 113 + export const serializePrimitiveParam = ({ 114 + allowReserved, 115 + name, 116 + value, 117 + }: SerializePrimitiveParam) => { 118 + if (value === undefined || value === null) { 119 + return ''; 120 + } 121 + 122 + if (typeof value === 'object') { 123 + throw new Error( 124 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 125 + ); 126 + } 127 + 128 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 129 + }; 130 + 131 + export const serializeObjectParam = ({ 132 + allowReserved, 133 + explode, 134 + name, 135 + style, 136 + value, 137 + valueOnly, 138 + }: SerializeOptions<ObjectSeparatorStyle> & { 139 + value: Record<string, unknown> | Date; 140 + valueOnly?: boolean; 141 + }) => { 142 + if (value instanceof Date) { 143 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 144 + } 145 + 146 + if (style !== 'deepObject' && !explode) { 147 + let values: string[] = []; 148 + Object.entries(value).forEach(([key, v]) => { 149 + values = [ 150 + ...values, 151 + key, 152 + allowReserved ? (v as string) : encodeURIComponent(v as string), 153 + ]; 154 + }); 155 + const joinedValues = values.join(','); 156 + switch (style) { 157 + case 'form': 158 + return `${name}=${joinedValues}`; 159 + case 'label': 160 + return `.${joinedValues}`; 161 + case 'matrix': 162 + return `;${name}=${joinedValues}`; 163 + default: 164 + return joinedValues; 165 + } 166 + } 167 + 168 + const separator = separatorObjectExplode(style); 169 + const joinedValues = Object.entries(value) 170 + .map(([key, v]) => 171 + serializePrimitiveParam({ 172 + allowReserved, 173 + name: style === 'deepObject' ? `${name}[${key}]` : key, 174 + value: v as string, 175 + }), 176 + ) 177 + .join(separator); 178 + return style === 'label' || style === 'matrix' 179 + ? separator + joinedValues 180 + : joinedValues; 181 + };
+136
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/core/queryKeySerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + /** 4 + * JSON-friendly union that mirrors what Pinia Colada can hash. 5 + */ 6 + export type JsonValue = 7 + | null 8 + | string 9 + | number 10 + | boolean 11 + | JsonValue[] 12 + | { [key: string]: JsonValue }; 13 + 14 + /** 15 + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. 16 + */ 17 + export const queryKeyJsonReplacer = (_key: string, value: unknown) => { 18 + if ( 19 + value === undefined || 20 + typeof value === 'function' || 21 + typeof value === 'symbol' 22 + ) { 23 + return undefined; 24 + } 25 + if (typeof value === 'bigint') { 26 + return value.toString(); 27 + } 28 + if (value instanceof Date) { 29 + return value.toISOString(); 30 + } 31 + return value; 32 + }; 33 + 34 + /** 35 + * Safely stringifies a value and parses it back into a JsonValue. 36 + */ 37 + export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { 38 + try { 39 + const json = JSON.stringify(input, queryKeyJsonReplacer); 40 + if (json === undefined) { 41 + return undefined; 42 + } 43 + return JSON.parse(json) as JsonValue; 44 + } catch { 45 + return undefined; 46 + } 47 + }; 48 + 49 + /** 50 + * Detects plain objects (including objects with a null prototype). 51 + */ 52 + const isPlainObject = (value: unknown): value is Record<string, unknown> => { 53 + if (value === null || typeof value !== 'object') { 54 + return false; 55 + } 56 + const prototype = Object.getPrototypeOf(value as object); 57 + return prototype === Object.prototype || prototype === null; 58 + }; 59 + 60 + /** 61 + * Turns URLSearchParams into a sorted JSON object for deterministic keys. 62 + */ 63 + const serializeSearchParams = (params: URLSearchParams): JsonValue => { 64 + const entries = Array.from(params.entries()).sort(([a], [b]) => 65 + a.localeCompare(b), 66 + ); 67 + const result: Record<string, JsonValue> = {}; 68 + 69 + for (const [key, value] of entries) { 70 + const existing = result[key]; 71 + if (existing === undefined) { 72 + result[key] = value; 73 + continue; 74 + } 75 + 76 + if (Array.isArray(existing)) { 77 + (existing as string[]).push(value); 78 + } else { 79 + result[key] = [existing, value]; 80 + } 81 + } 82 + 83 + return result; 84 + }; 85 + 86 + /** 87 + * Normalizes any accepted value into a JSON-friendly shape for query keys. 88 + */ 89 + export const serializeQueryKeyValue = ( 90 + value: unknown, 91 + ): JsonValue | undefined => { 92 + if (value === null) { 93 + return null; 94 + } 95 + 96 + if ( 97 + typeof value === 'string' || 98 + typeof value === 'number' || 99 + typeof value === 'boolean' 100 + ) { 101 + return value; 102 + } 103 + 104 + if ( 105 + value === undefined || 106 + typeof value === 'function' || 107 + typeof value === 'symbol' 108 + ) { 109 + return undefined; 110 + } 111 + 112 + if (typeof value === 'bigint') { 113 + return value.toString(); 114 + } 115 + 116 + if (value instanceof Date) { 117 + return value.toISOString(); 118 + } 119 + 120 + if (Array.isArray(value)) { 121 + return stringifyToJsonValue(value); 122 + } 123 + 124 + if ( 125 + typeof URLSearchParams !== 'undefined' && 126 + value instanceof URLSearchParams 127 + ) { 128 + return serializeSearchParams(value); 129 + } 130 + 131 + if (isPlainObject(value)) { 132 + return stringifyToJsonValue(value); 133 + } 134 + 135 + return undefined; 136 + };
+264
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/core/serverSentEvents.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Config } from './types.gen'; 4 + 5 + export type ServerSentEventsOptions<TData = unknown> = Omit< 6 + RequestInit, 7 + 'method' 8 + > & 9 + Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & { 10 + /** 11 + * Fetch API implementation. You can use this option to provide a custom 12 + * fetch instance. 13 + * 14 + * @default globalThis.fetch 15 + */ 16 + fetch?: typeof fetch; 17 + /** 18 + * Implementing clients can call request interceptors inside this hook. 19 + */ 20 + onRequest?: (url: string, init: RequestInit) => Promise<Request>; 21 + /** 22 + * Callback invoked when a network or parsing error occurs during streaming. 23 + * 24 + * This option applies only if the endpoint returns a stream of events. 25 + * 26 + * @param error The error that occurred. 27 + */ 28 + onSseError?: (error: unknown) => void; 29 + /** 30 + * Callback invoked when an event is streamed from the server. 31 + * 32 + * This option applies only if the endpoint returns a stream of events. 33 + * 34 + * @param event Event streamed from the server. 35 + * @returns Nothing (void). 36 + */ 37 + onSseEvent?: (event: StreamEvent<TData>) => void; 38 + serializedBody?: RequestInit['body']; 39 + /** 40 + * Default retry delay in milliseconds. 41 + * 42 + * This option applies only if the endpoint returns a stream of events. 43 + * 44 + * @default 3000 45 + */ 46 + sseDefaultRetryDelay?: number; 47 + /** 48 + * Maximum number of retry attempts before giving up. 49 + */ 50 + sseMaxRetryAttempts?: number; 51 + /** 52 + * Maximum retry delay in milliseconds. 53 + * 54 + * Applies only when exponential backoff is used. 55 + * 56 + * This option applies only if the endpoint returns a stream of events. 57 + * 58 + * @default 30000 59 + */ 60 + sseMaxRetryDelay?: number; 61 + /** 62 + * Optional sleep function for retry backoff. 63 + * 64 + * Defaults to using `setTimeout`. 65 + */ 66 + sseSleepFn?: (ms: number) => Promise<void>; 67 + url: string; 68 + }; 69 + 70 + export interface StreamEvent<TData = unknown> { 71 + data: TData; 72 + event?: string; 73 + id?: string; 74 + retry?: number; 75 + } 76 + 77 + export type ServerSentEventsResult< 78 + TData = unknown, 79 + TReturn = void, 80 + TNext = unknown, 81 + > = { 82 + stream: AsyncGenerator< 83 + TData extends Record<string, unknown> ? TData[keyof TData] : TData, 84 + TReturn, 85 + TNext 86 + >; 87 + }; 88 + 89 + export const createSseClient = <TData = unknown>({ 90 + onRequest, 91 + onSseError, 92 + onSseEvent, 93 + responseTransformer, 94 + responseValidator, 95 + sseDefaultRetryDelay, 96 + sseMaxRetryAttempts, 97 + sseMaxRetryDelay, 98 + sseSleepFn, 99 + url, 100 + ...options 101 + }: ServerSentEventsOptions): ServerSentEventsResult<TData> => { 102 + let lastEventId: string | undefined; 103 + 104 + const sleep = 105 + sseSleepFn ?? 106 + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); 107 + 108 + const createStream = async function* () { 109 + let retryDelay: number = sseDefaultRetryDelay ?? 3000; 110 + let attempt = 0; 111 + const signal = options.signal ?? new AbortController().signal; 112 + 113 + while (true) { 114 + if (signal.aborted) break; 115 + 116 + attempt++; 117 + 118 + const headers = 119 + options.headers instanceof Headers 120 + ? options.headers 121 + : new Headers(options.headers as Record<string, string> | undefined); 122 + 123 + if (lastEventId !== undefined) { 124 + headers.set('Last-Event-ID', lastEventId); 125 + } 126 + 127 + try { 128 + const requestInit: RequestInit = { 129 + redirect: 'follow', 130 + ...options, 131 + body: options.serializedBody, 132 + headers, 133 + signal, 134 + }; 135 + let request = new Request(url, requestInit); 136 + if (onRequest) { 137 + request = await onRequest(url, requestInit); 138 + } 139 + // fetch must be assigned here, otherwise it would throw the error: 140 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 141 + const _fetch = options.fetch ?? globalThis.fetch; 142 + const response = await _fetch(request); 143 + 144 + if (!response.ok) 145 + throw new Error( 146 + `SSE failed: ${response.status} ${response.statusText}`, 147 + ); 148 + 149 + if (!response.body) throw new Error('No body in SSE response'); 150 + 151 + const reader = response.body 152 + .pipeThrough(new TextDecoderStream()) 153 + .getReader(); 154 + 155 + let buffer = ''; 156 + 157 + const abortHandler = () => { 158 + try { 159 + reader.cancel(); 160 + } catch { 161 + // noop 162 + } 163 + }; 164 + 165 + signal.addEventListener('abort', abortHandler); 166 + 167 + try { 168 + while (true) { 169 + const { done, value } = await reader.read(); 170 + if (done) break; 171 + buffer += value; 172 + 173 + const chunks = buffer.split('\n\n'); 174 + buffer = chunks.pop() ?? ''; 175 + 176 + for (const chunk of chunks) { 177 + const lines = chunk.split('\n'); 178 + const dataLines: Array<string> = []; 179 + let eventName: string | undefined; 180 + 181 + for (const line of lines) { 182 + if (line.startsWith('data:')) { 183 + dataLines.push(line.replace(/^data:\s*/, '')); 184 + } else if (line.startsWith('event:')) { 185 + eventName = line.replace(/^event:\s*/, ''); 186 + } else if (line.startsWith('id:')) { 187 + lastEventId = line.replace(/^id:\s*/, ''); 188 + } else if (line.startsWith('retry:')) { 189 + const parsed = Number.parseInt( 190 + line.replace(/^retry:\s*/, ''), 191 + 10, 192 + ); 193 + if (!Number.isNaN(parsed)) { 194 + retryDelay = parsed; 195 + } 196 + } 197 + } 198 + 199 + let data: unknown; 200 + let parsedJson = false; 201 + 202 + if (dataLines.length) { 203 + const rawData = dataLines.join('\n'); 204 + try { 205 + data = JSON.parse(rawData); 206 + parsedJson = true; 207 + } catch { 208 + data = rawData; 209 + } 210 + } 211 + 212 + if (parsedJson) { 213 + if (responseValidator) { 214 + await responseValidator(data); 215 + } 216 + 217 + if (responseTransformer) { 218 + data = await responseTransformer(data); 219 + } 220 + } 221 + 222 + onSseEvent?.({ 223 + data, 224 + event: eventName, 225 + id: lastEventId, 226 + retry: retryDelay, 227 + }); 228 + 229 + if (dataLines.length) { 230 + yield data as any; 231 + } 232 + } 233 + } 234 + } finally { 235 + signal.removeEventListener('abort', abortHandler); 236 + reader.releaseLock(); 237 + } 238 + 239 + break; // exit loop on normal completion 240 + } catch (error) { 241 + // connection failed or aborted; retry after delay 242 + onSseError?.(error); 243 + 244 + if ( 245 + sseMaxRetryAttempts !== undefined && 246 + attempt >= sseMaxRetryAttempts 247 + ) { 248 + break; // stop after firing error 249 + } 250 + 251 + // exponential backoff: double retry each attempt, cap at 30s 252 + const backoff = Math.min( 253 + retryDelay * 2 ** (attempt - 1), 254 + sseMaxRetryDelay ?? 30000, 255 + ); 256 + await sleep(backoff); 257 + } 258 + } 259 + }; 260 + 261 + const stream = createStream(); 262 + 263 + return { stream }; 264 + };
+118
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/core/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Auth, AuthToken } from './auth.gen'; 4 + import type { 5 + BodySerializer, 6 + QuerySerializer, 7 + QuerySerializerOptions, 8 + } from './bodySerializer.gen'; 9 + 10 + export type HttpMethod = 11 + | 'connect' 12 + | 'delete' 13 + | 'get' 14 + | 'head' 15 + | 'options' 16 + | 'patch' 17 + | 'post' 18 + | 'put' 19 + | 'trace'; 20 + 21 + export type Client< 22 + RequestFn = never, 23 + Config = unknown, 24 + MethodFn = never, 25 + BuildUrlFn = never, 26 + SseFn = never, 27 + > = { 28 + /** 29 + * Returns the final request URL. 30 + */ 31 + buildUrl: BuildUrlFn; 32 + getConfig: () => Config; 33 + request: RequestFn; 34 + setConfig: (config: Config) => Config; 35 + } & { 36 + [K in HttpMethod]: MethodFn; 37 + } & ([SseFn] extends [never] 38 + ? { sse?: never } 39 + : { sse: { [K in HttpMethod]: SseFn } }); 40 + 41 + export interface Config { 42 + /** 43 + * Auth token or a function returning auth token. The resolved value will be 44 + * added to the request payload as defined by its `security` array. 45 + */ 46 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 47 + /** 48 + * A function for serializing request body parameter. By default, 49 + * {@link JSON.stringify()} will be used. 50 + */ 51 + bodySerializer?: BodySerializer | null; 52 + /** 53 + * An object containing any HTTP headers that you want to pre-populate your 54 + * `Headers` object with. 55 + * 56 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 57 + */ 58 + headers?: 59 + | RequestInit['headers'] 60 + | Record< 61 + string, 62 + | string 63 + | number 64 + | boolean 65 + | (string | number | boolean)[] 66 + | null 67 + | undefined 68 + | unknown 69 + >; 70 + /** 71 + * The request method. 72 + * 73 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 74 + */ 75 + method?: Uppercase<HttpMethod>; 76 + /** 77 + * A function for serializing request query parameters. By default, arrays 78 + * will be exploded in form style, objects will be exploded in deepObject 79 + * style, and reserved characters are percent-encoded. 80 + * 81 + * This method will have no effect if the native `paramsSerializer()` Axios 82 + * API function is used. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 87 + /** 88 + * A function validating request data. This is useful if you want to ensure 89 + * the request conforms to the desired shape, so it can be safely sent to 90 + * the server. 91 + */ 92 + requestValidator?: (data: unknown) => Promise<unknown>; 93 + /** 94 + * A function transforming response data before it's returned. This is useful 95 + * for post-processing data, e.g. converting ISO strings into Date objects. 96 + */ 97 + responseTransformer?: (data: unknown) => Promise<unknown>; 98 + /** 99 + * A function validating response data. This is useful if you want to ensure 100 + * the response conforms to the desired shape, so it can be safely passed to 101 + * the transformers and returned to the user. 102 + */ 103 + responseValidator?: (data: unknown) => Promise<unknown>; 104 + } 105 + 106 + type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never] 107 + ? true 108 + : [T] extends [never | undefined] 109 + ? [undefined] extends [T] 110 + ? false 111 + : true 112 + : false; 113 + 114 + export type OmitNever<T extends Record<string, unknown>> = { 115 + [K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true 116 + ? never 117 + : K]: T[K]; 118 + };
+143
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/core/utils.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; 4 + import { 5 + type ArraySeparatorStyle, 6 + serializeArrayParam, 7 + serializeObjectParam, 8 + serializePrimitiveParam, 9 + } from './pathSerializer.gen'; 10 + 11 + export interface PathSerializer { 12 + path: Record<string, unknown>; 13 + url: string; 14 + } 15 + 16 + export const PATH_PARAM_RE = /\{[^{}]+\}/g; 17 + 18 + export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 19 + let url = _url; 20 + const matches = _url.match(PATH_PARAM_RE); 21 + if (matches) { 22 + for (const match of matches) { 23 + let explode = false; 24 + let name = match.substring(1, match.length - 1); 25 + let style: ArraySeparatorStyle = 'simple'; 26 + 27 + if (name.endsWith('*')) { 28 + explode = true; 29 + name = name.substring(0, name.length - 1); 30 + } 31 + 32 + if (name.startsWith('.')) { 33 + name = name.substring(1); 34 + style = 'label'; 35 + } else if (name.startsWith(';')) { 36 + name = name.substring(1); 37 + style = 'matrix'; 38 + } 39 + 40 + const value = path[name]; 41 + 42 + if (value === undefined || value === null) { 43 + continue; 44 + } 45 + 46 + if (Array.isArray(value)) { 47 + url = url.replace( 48 + match, 49 + serializeArrayParam({ explode, name, style, value }), 50 + ); 51 + continue; 52 + } 53 + 54 + if (typeof value === 'object') { 55 + url = url.replace( 56 + match, 57 + serializeObjectParam({ 58 + explode, 59 + name, 60 + style, 61 + value: value as Record<string, unknown>, 62 + valueOnly: true, 63 + }), 64 + ); 65 + continue; 66 + } 67 + 68 + if (style === 'matrix') { 69 + url = url.replace( 70 + match, 71 + `;${serializePrimitiveParam({ 72 + name, 73 + value: value as string, 74 + })}`, 75 + ); 76 + continue; 77 + } 78 + 79 + const replaceValue = encodeURIComponent( 80 + style === 'label' ? `.${value as string}` : (value as string), 81 + ); 82 + url = url.replace(match, replaceValue); 83 + } 84 + } 85 + return url; 86 + }; 87 + 88 + export const getUrl = ({ 89 + baseUrl, 90 + path, 91 + query, 92 + querySerializer, 93 + url: _url, 94 + }: { 95 + baseUrl?: string; 96 + path?: Record<string, unknown>; 97 + query?: Record<string, unknown>; 98 + querySerializer: QuerySerializer; 99 + url: string; 100 + }) => { 101 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 102 + let url = (baseUrl ?? '') + pathUrl; 103 + if (path) { 104 + url = defaultPathSerializer({ path, url }); 105 + } 106 + let search = query ? querySerializer(query) : ''; 107 + if (search.startsWith('?')) { 108 + search = search.substring(1); 109 + } 110 + if (search) { 111 + url += `?${search}`; 112 + } 113 + return url; 114 + }; 115 + 116 + export function getValidRequestBody(options: { 117 + body?: unknown; 118 + bodySerializer?: BodySerializer | null; 119 + serializedBody?: unknown; 120 + }) { 121 + const hasBody = options.body !== undefined; 122 + const isSerializedBody = hasBody && options.bodySerializer; 123 + 124 + if (isSerializedBody) { 125 + if ('serializedBody' in options) { 126 + const hasSerializedBody = 127 + options.serializedBody !== undefined && options.serializedBody !== ''; 128 + 129 + return hasSerializedBody ? options.serializedBody : null; 130 + } 131 + 132 + // not all clients implement a serializedBody property (i.e. client-axios) 133 + return options.body !== '' ? options.body : null; 134 + } 135 + 136 + // plain/text body 137 + if (hasBody) { 138 + return options.body; 139 + } 140 + 141 + // no body was provided 142 + return undefined; 143 + }
+3
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type * from './types.gen';
+22
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/transformers.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { GetTreeResponse } from './types.gen'; 4 + 5 + const treeNodeSchemaResponseTransformer = (data: any) => { 6 + if (data.createdAt) { 7 + data.createdAt = new Date(data.createdAt); 8 + } 9 + if (data.children) { 10 + data.children = data.children.map((item: any) => { 11 + return treeNodeSchemaResponseTransformer(item); 12 + }); 13 + } 14 + return data; 15 + }; 16 + 17 + export const getTreeResponseTransformer = async (data: any): Promise<GetTreeResponse> => { 18 + data = data.map((item: any) => { 19 + return treeNodeSchemaResponseTransformer(item); 20 + }); 21 + return data; 22 + };
+28
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-recursive/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type ClientOptions = { 4 + baseUrl: `${string}://${string}` | (string & {}); 5 + }; 6 + 7 + export type TreeNode = { 8 + id?: string; 9 + name?: string; 10 + createdAt?: Date; 11 + children?: Array<TreeNode>; 12 + }; 13 + 14 + export type GetTreeData = { 15 + body?: never; 16 + path?: never; 17 + query?: never; 18 + url: '/tree'; 19 + }; 20 + 21 + export type GetTreeResponses = { 22 + /** 23 + * Success 24 + */ 25 + 200: Array<TreeNode>; 26 + }; 27 + 28 + export type GetTreeResponse = GetTreeResponses[keyof GetTreeResponses];
+16
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/client.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { type ClientOptions, type Config, createClient, createConfig } from './client'; 4 + import type { ClientOptions as ClientOptions2 } from './types.gen'; 5 + 6 + /** 7 + * The `createClientConfig()` function will be called on client initialization 8 + * and the returned object will become the client's initial configuration. 9 + * 10 + * You may want to initialize your client this way instead of calling 11 + * `setConfig()`. This is useful for example if you're using Next.js 12 + * to ensure your client always has the correct values. 13 + */ 14 + export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>; 15 + 16 + export const client = createClient(createConfig<ClientOptions2>());
+268
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/client/client.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { createSseClient } from '../core/serverSentEvents.gen'; 4 + import type { HttpMethod } from '../core/types.gen'; 5 + import { getValidRequestBody } from '../core/utils.gen'; 6 + import type { 7 + Client, 8 + Config, 9 + RequestOptions, 10 + ResolvedRequestOptions, 11 + } from './types.gen'; 12 + import { 13 + buildUrl, 14 + createConfig, 15 + createInterceptors, 16 + getParseAs, 17 + mergeConfigs, 18 + mergeHeaders, 19 + setAuthParams, 20 + } from './utils.gen'; 21 + 22 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 23 + body?: any; 24 + headers: ReturnType<typeof mergeHeaders>; 25 + }; 26 + 27 + export const createClient = (config: Config = {}): Client => { 28 + let _config = mergeConfigs(createConfig(), config); 29 + 30 + const getConfig = (): Config => ({ ..._config }); 31 + 32 + const setConfig = (config: Config): Config => { 33 + _config = mergeConfigs(_config, config); 34 + return getConfig(); 35 + }; 36 + 37 + const interceptors = createInterceptors< 38 + Request, 39 + Response, 40 + unknown, 41 + ResolvedRequestOptions 42 + >(); 43 + 44 + const beforeRequest = async (options: RequestOptions) => { 45 + const opts = { 46 + ..._config, 47 + ...options, 48 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 49 + headers: mergeHeaders(_config.headers, options.headers), 50 + serializedBody: undefined, 51 + }; 52 + 53 + if (opts.security) { 54 + await setAuthParams({ 55 + ...opts, 56 + security: opts.security, 57 + }); 58 + } 59 + 60 + if (opts.requestValidator) { 61 + await opts.requestValidator(opts); 62 + } 63 + 64 + if (opts.body !== undefined && opts.bodySerializer) { 65 + opts.serializedBody = opts.bodySerializer(opts.body); 66 + } 67 + 68 + // remove Content-Type header if body is empty to avoid sending invalid requests 69 + if (opts.body === undefined || opts.serializedBody === '') { 70 + opts.headers.delete('Content-Type'); 71 + } 72 + 73 + const url = buildUrl(opts); 74 + 75 + return { opts, url }; 76 + }; 77 + 78 + const request: Client['request'] = async (options) => { 79 + // @ts-expect-error 80 + const { opts, url } = await beforeRequest(options); 81 + const requestInit: ReqInit = { 82 + redirect: 'follow', 83 + ...opts, 84 + body: getValidRequestBody(opts), 85 + }; 86 + 87 + let request = new Request(url, requestInit); 88 + 89 + for (const fn of interceptors.request.fns) { 90 + if (fn) { 91 + request = await fn(request, opts); 92 + } 93 + } 94 + 95 + // fetch must be assigned here, otherwise it would throw the error: 96 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 97 + const _fetch = opts.fetch!; 98 + let response = await _fetch(request); 99 + 100 + for (const fn of interceptors.response.fns) { 101 + if (fn) { 102 + response = await fn(response, request, opts); 103 + } 104 + } 105 + 106 + const result = { 107 + request, 108 + response, 109 + }; 110 + 111 + if (response.ok) { 112 + const parseAs = 113 + (opts.parseAs === 'auto' 114 + ? getParseAs(response.headers.get('Content-Type')) 115 + : opts.parseAs) ?? 'json'; 116 + 117 + if ( 118 + response.status === 204 || 119 + response.headers.get('Content-Length') === '0' 120 + ) { 121 + let emptyData: any; 122 + switch (parseAs) { 123 + case 'arrayBuffer': 124 + case 'blob': 125 + case 'text': 126 + emptyData = await response[parseAs](); 127 + break; 128 + case 'formData': 129 + emptyData = new FormData(); 130 + break; 131 + case 'stream': 132 + emptyData = response.body; 133 + break; 134 + case 'json': 135 + default: 136 + emptyData = {}; 137 + break; 138 + } 139 + return opts.responseStyle === 'data' 140 + ? emptyData 141 + : { 142 + data: emptyData, 143 + ...result, 144 + }; 145 + } 146 + 147 + let data: any; 148 + switch (parseAs) { 149 + case 'arrayBuffer': 150 + case 'blob': 151 + case 'formData': 152 + case 'json': 153 + case 'text': 154 + data = await response[parseAs](); 155 + break; 156 + case 'stream': 157 + return opts.responseStyle === 'data' 158 + ? response.body 159 + : { 160 + data: response.body, 161 + ...result, 162 + }; 163 + } 164 + 165 + if (parseAs === 'json') { 166 + if (opts.responseValidator) { 167 + await opts.responseValidator(data); 168 + } 169 + 170 + if (opts.responseTransformer) { 171 + data = await opts.responseTransformer(data); 172 + } 173 + } 174 + 175 + return opts.responseStyle === 'data' 176 + ? data 177 + : { 178 + data, 179 + ...result, 180 + }; 181 + } 182 + 183 + const textError = await response.text(); 184 + let jsonError: unknown; 185 + 186 + try { 187 + jsonError = JSON.parse(textError); 188 + } catch { 189 + // noop 190 + } 191 + 192 + const error = jsonError ?? textError; 193 + let finalError = error; 194 + 195 + for (const fn of interceptors.error.fns) { 196 + if (fn) { 197 + finalError = (await fn(error, response, request, opts)) as string; 198 + } 199 + } 200 + 201 + finalError = finalError || ({} as string); 202 + 203 + if (opts.throwOnError) { 204 + throw finalError; 205 + } 206 + 207 + // TODO: we probably want to return error and improve types 208 + return opts.responseStyle === 'data' 209 + ? undefined 210 + : { 211 + error: finalError, 212 + ...result, 213 + }; 214 + }; 215 + 216 + const makeMethodFn = 217 + (method: Uppercase<HttpMethod>) => (options: RequestOptions) => 218 + request({ ...options, method }); 219 + 220 + const makeSseFn = 221 + (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => { 222 + const { opts, url } = await beforeRequest(options); 223 + return createSseClient({ 224 + ...opts, 225 + body: opts.body as BodyInit | null | undefined, 226 + headers: opts.headers as unknown as Record<string, string>, 227 + method, 228 + onRequest: async (url, init) => { 229 + let request = new Request(url, init); 230 + for (const fn of interceptors.request.fns) { 231 + if (fn) { 232 + request = await fn(request, opts); 233 + } 234 + } 235 + return request; 236 + }, 237 + url, 238 + }); 239 + }; 240 + 241 + return { 242 + buildUrl, 243 + connect: makeMethodFn('CONNECT'), 244 + delete: makeMethodFn('DELETE'), 245 + get: makeMethodFn('GET'), 246 + getConfig, 247 + head: makeMethodFn('HEAD'), 248 + interceptors, 249 + options: makeMethodFn('OPTIONS'), 250 + patch: makeMethodFn('PATCH'), 251 + post: makeMethodFn('POST'), 252 + put: makeMethodFn('PUT'), 253 + request, 254 + setConfig, 255 + sse: { 256 + connect: makeSseFn('CONNECT'), 257 + delete: makeSseFn('DELETE'), 258 + get: makeSseFn('GET'), 259 + head: makeSseFn('HEAD'), 260 + options: makeSseFn('OPTIONS'), 261 + patch: makeSseFn('PATCH'), 262 + post: makeSseFn('POST'), 263 + put: makeSseFn('PUT'), 264 + trace: makeSseFn('TRACE'), 265 + }, 266 + trace: makeMethodFn('TRACE'), 267 + } as Client; 268 + };
+26
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/client/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { Auth } from '../core/auth.gen'; 4 + export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; 5 + export { 6 + formDataBodySerializer, 7 + jsonBodySerializer, 8 + urlSearchParamsBodySerializer, 9 + } from '../core/bodySerializer.gen'; 10 + export { buildClientParams } from '../core/params.gen'; 11 + export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; 12 + export { createClient } from './client.gen'; 13 + export type { 14 + Client, 15 + ClientOptions, 16 + Config, 17 + CreateClientConfig, 18 + Options, 19 + OptionsLegacyParser, 20 + RequestOptions, 21 + RequestResult, 22 + ResolvedRequestOptions, 23 + ResponseStyle, 24 + TDataShape, 25 + } from './types.gen'; 26 + export { createConfig, mergeHeaders } from './utils.gen';
+268
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/client/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Auth } from '../core/auth.gen'; 4 + import type { 5 + ServerSentEventsOptions, 6 + ServerSentEventsResult, 7 + } from '../core/serverSentEvents.gen'; 8 + import type { 9 + Client as CoreClient, 10 + Config as CoreConfig, 11 + } from '../core/types.gen'; 12 + import type { Middleware } from './utils.gen'; 13 + 14 + export type ResponseStyle = 'data' | 'fields'; 15 + 16 + export interface Config<T extends ClientOptions = ClientOptions> 17 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, 18 + CoreConfig { 19 + /** 20 + * Base URL for all requests made by this client. 21 + */ 22 + baseUrl?: T['baseUrl']; 23 + /** 24 + * Fetch API implementation. You can use this option to provide a custom 25 + * fetch instance. 26 + * 27 + * @default globalThis.fetch 28 + */ 29 + fetch?: typeof fetch; 30 + /** 31 + * Please don't use the Fetch client for Next.js applications. The `next` 32 + * options won't have any effect. 33 + * 34 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 35 + */ 36 + next?: never; 37 + /** 38 + * Return the response data parsed in a specified format. By default, `auto` 39 + * will infer the appropriate method from the `Content-Type` response header. 40 + * You can override this behavior with any of the {@link Body} methods. 41 + * Select `stream` if you don't want to parse response data at all. 42 + * 43 + * @default 'auto' 44 + */ 45 + parseAs?: 46 + | 'arrayBuffer' 47 + | 'auto' 48 + | 'blob' 49 + | 'formData' 50 + | 'json' 51 + | 'stream' 52 + | 'text'; 53 + /** 54 + * Should we return only data or multiple fields (data, error, response, etc.)? 55 + * 56 + * @default 'fields' 57 + */ 58 + responseStyle?: ResponseStyle; 59 + /** 60 + * Throw an error instead of returning it in the response? 61 + * 62 + * @default false 63 + */ 64 + throwOnError?: T['throwOnError']; 65 + } 66 + 67 + export interface RequestOptions< 68 + TData = unknown, 69 + TResponseStyle extends ResponseStyle = 'fields', 70 + ThrowOnError extends boolean = boolean, 71 + Url extends string = string, 72 + > extends Config<{ 73 + responseStyle: TResponseStyle; 74 + throwOnError: ThrowOnError; 75 + }>, 76 + Pick< 77 + ServerSentEventsOptions<TData>, 78 + | 'onSseError' 79 + | 'onSseEvent' 80 + | 'sseDefaultRetryDelay' 81 + | 'sseMaxRetryAttempts' 82 + | 'sseMaxRetryDelay' 83 + > { 84 + /** 85 + * Any body that you want to add to your request. 86 + * 87 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 88 + */ 89 + body?: unknown; 90 + path?: Record<string, unknown>; 91 + query?: Record<string, unknown>; 92 + /** 93 + * Security mechanism(s) to use for the request. 94 + */ 95 + security?: ReadonlyArray<Auth>; 96 + url: Url; 97 + } 98 + 99 + export interface ResolvedRequestOptions< 100 + TResponseStyle extends ResponseStyle = 'fields', 101 + ThrowOnError extends boolean = boolean, 102 + Url extends string = string, 103 + > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> { 104 + serializedBody?: string; 105 + } 106 + 107 + export type RequestResult< 108 + TData = unknown, 109 + TError = unknown, 110 + ThrowOnError extends boolean = boolean, 111 + TResponseStyle extends ResponseStyle = 'fields', 112 + > = ThrowOnError extends true 113 + ? Promise< 114 + TResponseStyle extends 'data' 115 + ? TData extends Record<string, unknown> 116 + ? TData[keyof TData] 117 + : TData 118 + : { 119 + data: TData extends Record<string, unknown> 120 + ? TData[keyof TData] 121 + : TData; 122 + request: Request; 123 + response: Response; 124 + } 125 + > 126 + : Promise< 127 + TResponseStyle extends 'data' 128 + ? 129 + | (TData extends Record<string, unknown> 130 + ? TData[keyof TData] 131 + : TData) 132 + | undefined 133 + : ( 134 + | { 135 + data: TData extends Record<string, unknown> 136 + ? TData[keyof TData] 137 + : TData; 138 + error: undefined; 139 + } 140 + | { 141 + data: undefined; 142 + error: TError extends Record<string, unknown> 143 + ? TError[keyof TError] 144 + : TError; 145 + } 146 + ) & { 147 + request: Request; 148 + response: Response; 149 + } 150 + >; 151 + 152 + export interface ClientOptions { 153 + baseUrl?: string; 154 + responseStyle?: ResponseStyle; 155 + throwOnError?: boolean; 156 + } 157 + 158 + type MethodFn = < 159 + TData = unknown, 160 + TError = unknown, 161 + ThrowOnError extends boolean = false, 162 + TResponseStyle extends ResponseStyle = 'fields', 163 + >( 164 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 165 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 166 + 167 + type SseFn = < 168 + TData = unknown, 169 + TError = unknown, 170 + ThrowOnError extends boolean = false, 171 + TResponseStyle extends ResponseStyle = 'fields', 172 + >( 173 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 174 + ) => Promise<ServerSentEventsResult<TData, TError>>; 175 + 176 + type RequestFn = < 177 + TData = unknown, 178 + TError = unknown, 179 + ThrowOnError extends boolean = false, 180 + TResponseStyle extends ResponseStyle = 'fields', 181 + >( 182 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> & 183 + Pick< 184 + Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 185 + 'method' 186 + >, 187 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 188 + 189 + type BuildUrlFn = < 190 + TData extends { 191 + body?: unknown; 192 + path?: Record<string, unknown>; 193 + query?: Record<string, unknown>; 194 + url: string; 195 + }, 196 + >( 197 + options: Pick<TData, 'url'> & Options<TData>, 198 + ) => string; 199 + 200 + export type Client = CoreClient< 201 + RequestFn, 202 + Config, 203 + MethodFn, 204 + BuildUrlFn, 205 + SseFn 206 + > & { 207 + interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>; 208 + }; 209 + 210 + /** 211 + * The `createClientConfig()` function will be called on client initialization 212 + * and the returned object will become the client's initial configuration. 213 + * 214 + * You may want to initialize your client this way instead of calling 215 + * `setConfig()`. This is useful for example if you're using Next.js 216 + * to ensure your client always has the correct values. 217 + */ 218 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 219 + override?: Config<ClientOptions & T>, 220 + ) => Config<Required<ClientOptions> & T>; 221 + 222 + export interface TDataShape { 223 + body?: unknown; 224 + headers?: unknown; 225 + path?: unknown; 226 + query?: unknown; 227 + url: string; 228 + } 229 + 230 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 231 + 232 + export type Options< 233 + TData extends TDataShape = TDataShape, 234 + ThrowOnError extends boolean = boolean, 235 + TResponse = unknown, 236 + TResponseStyle extends ResponseStyle = 'fields', 237 + > = OmitKeys< 238 + RequestOptions<TResponse, TResponseStyle, ThrowOnError>, 239 + 'body' | 'path' | 'query' | 'url' 240 + > & 241 + Omit<TData, 'url'>; 242 + 243 + export type OptionsLegacyParser< 244 + TData = unknown, 245 + ThrowOnError extends boolean = boolean, 246 + TResponseStyle extends ResponseStyle = 'fields', 247 + > = TData extends { body?: any } 248 + ? TData extends { headers?: any } 249 + ? OmitKeys< 250 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 251 + 'body' | 'headers' | 'url' 252 + > & 253 + TData 254 + : OmitKeys< 255 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 256 + 'body' | 'url' 257 + > & 258 + TData & 259 + Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'headers'> 260 + : TData extends { headers?: any } 261 + ? OmitKeys< 262 + RequestOptions<unknown, TResponseStyle, ThrowOnError>, 263 + 'headers' | 'url' 264 + > & 265 + TData & 266 + Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'body'> 267 + : OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'url'> & 268 + TData;
+332
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/client/utils.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { getAuthToken } from '../core/auth.gen'; 4 + import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; 5 + import { jsonBodySerializer } from '../core/bodySerializer.gen'; 6 + import { 7 + serializeArrayParam, 8 + serializeObjectParam, 9 + serializePrimitiveParam, 10 + } from '../core/pathSerializer.gen'; 11 + import { getUrl } from '../core/utils.gen'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; 13 + 14 + export const createQuerySerializer = <T = unknown>({ 15 + parameters = {}, 16 + ...args 17 + }: QuerySerializerOptions = {}) => { 18 + const querySerializer = (queryParams: T) => { 19 + const search: string[] = []; 20 + if (queryParams && typeof queryParams === 'object') { 21 + for (const name in queryParams) { 22 + const value = queryParams[name]; 23 + 24 + if (value === undefined || value === null) { 25 + continue; 26 + } 27 + 28 + const options = parameters[name] || args; 29 + 30 + if (Array.isArray(value)) { 31 + const serializedArray = serializeArrayParam({ 32 + allowReserved: options.allowReserved, 33 + explode: true, 34 + name, 35 + style: 'form', 36 + value, 37 + ...options.array, 38 + }); 39 + if (serializedArray) search.push(serializedArray); 40 + } else if (typeof value === 'object') { 41 + const serializedObject = serializeObjectParam({ 42 + allowReserved: options.allowReserved, 43 + explode: true, 44 + name, 45 + style: 'deepObject', 46 + value: value as Record<string, unknown>, 47 + ...options.object, 48 + }); 49 + if (serializedObject) search.push(serializedObject); 50 + } else { 51 + const serializedPrimitive = serializePrimitiveParam({ 52 + allowReserved: options.allowReserved, 53 + name, 54 + value: value as string, 55 + }); 56 + if (serializedPrimitive) search.push(serializedPrimitive); 57 + } 58 + } 59 + } 60 + return search.join('&'); 61 + }; 62 + return querySerializer; 63 + }; 64 + 65 + /** 66 + * Infers parseAs value from provided Content-Type header. 67 + */ 68 + export const getParseAs = ( 69 + contentType: string | null, 70 + ): Exclude<Config['parseAs'], 'auto'> => { 71 + if (!contentType) { 72 + // If no Content-Type header is provided, the best we can do is return the raw response body, 73 + // which is effectively the same as the 'stream' option. 74 + return 'stream'; 75 + } 76 + 77 + const cleanContent = contentType.split(';')[0]?.trim(); 78 + 79 + if (!cleanContent) { 80 + return; 81 + } 82 + 83 + if ( 84 + cleanContent.startsWith('application/json') || 85 + cleanContent.endsWith('+json') 86 + ) { 87 + return 'json'; 88 + } 89 + 90 + if (cleanContent === 'multipart/form-data') { 91 + return 'formData'; 92 + } 93 + 94 + if ( 95 + ['application/', 'audio/', 'image/', 'video/'].some((type) => 96 + cleanContent.startsWith(type), 97 + ) 98 + ) { 99 + return 'blob'; 100 + } 101 + 102 + if (cleanContent.startsWith('text/')) { 103 + return 'text'; 104 + } 105 + 106 + return; 107 + }; 108 + 109 + const checkForExistence = ( 110 + options: Pick<RequestOptions, 'auth' | 'query'> & { 111 + headers: Headers; 112 + }, 113 + name?: string, 114 + ): boolean => { 115 + if (!name) { 116 + return false; 117 + } 118 + if ( 119 + options.headers.has(name) || 120 + options.query?.[name] || 121 + options.headers.get('Cookie')?.includes(`${name}=`) 122 + ) { 123 + return true; 124 + } 125 + return false; 126 + }; 127 + 128 + export const setAuthParams = async ({ 129 + security, 130 + ...options 131 + }: Pick<Required<RequestOptions>, 'security'> & 132 + Pick<RequestOptions, 'auth' | 'query'> & { 133 + headers: Headers; 134 + }) => { 135 + for (const auth of security) { 136 + if (checkForExistence(options, auth.name)) { 137 + continue; 138 + } 139 + 140 + const token = await getAuthToken(auth, options.auth); 141 + 142 + if (!token) { 143 + continue; 144 + } 145 + 146 + const name = auth.name ?? 'Authorization'; 147 + 148 + switch (auth.in) { 149 + case 'query': 150 + if (!options.query) { 151 + options.query = {}; 152 + } 153 + options.query[name] = token; 154 + break; 155 + case 'cookie': 156 + options.headers.append('Cookie', `${name}=${token}`); 157 + break; 158 + case 'header': 159 + default: 160 + options.headers.set(name, token); 161 + break; 162 + } 163 + } 164 + }; 165 + 166 + export const buildUrl: Client['buildUrl'] = (options) => 167 + getUrl({ 168 + baseUrl: options.baseUrl as string, 169 + path: options.path, 170 + query: options.query, 171 + querySerializer: 172 + typeof options.querySerializer === 'function' 173 + ? options.querySerializer 174 + : createQuerySerializer(options.querySerializer), 175 + url: options.url, 176 + }); 177 + 178 + export const mergeConfigs = (a: Config, b: Config): Config => { 179 + const config = { ...a, ...b }; 180 + if (config.baseUrl?.endsWith('/')) { 181 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 182 + } 183 + config.headers = mergeHeaders(a.headers, b.headers); 184 + return config; 185 + }; 186 + 187 + const headersEntries = (headers: Headers): Array<[string, string]> => { 188 + const entries: Array<[string, string]> = []; 189 + headers.forEach((value, key) => { 190 + entries.push([key, value]); 191 + }); 192 + return entries; 193 + }; 194 + 195 + export const mergeHeaders = ( 196 + ...headers: Array<Required<Config>['headers'] | undefined> 197 + ): Headers => { 198 + const mergedHeaders = new Headers(); 199 + for (const header of headers) { 200 + if (!header) { 201 + continue; 202 + } 203 + 204 + const iterator = 205 + header instanceof Headers 206 + ? headersEntries(header) 207 + : Object.entries(header); 208 + 209 + for (const [key, value] of iterator) { 210 + if (value === null) { 211 + mergedHeaders.delete(key); 212 + } else if (Array.isArray(value)) { 213 + for (const v of value) { 214 + mergedHeaders.append(key, v as string); 215 + } 216 + } else if (value !== undefined) { 217 + // assume object headers are meant to be JSON stringified, i.e. their 218 + // content value in OpenAPI specification is 'application/json' 219 + mergedHeaders.set( 220 + key, 221 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 222 + ); 223 + } 224 + } 225 + } 226 + return mergedHeaders; 227 + }; 228 + 229 + type ErrInterceptor<Err, Res, Req, Options> = ( 230 + error: Err, 231 + response: Res, 232 + request: Req, 233 + options: Options, 234 + ) => Err | Promise<Err>; 235 + 236 + type ReqInterceptor<Req, Options> = ( 237 + request: Req, 238 + options: Options, 239 + ) => Req | Promise<Req>; 240 + 241 + type ResInterceptor<Res, Req, Options> = ( 242 + response: Res, 243 + request: Req, 244 + options: Options, 245 + ) => Res | Promise<Res>; 246 + 247 + class Interceptors<Interceptor> { 248 + fns: Array<Interceptor | null> = []; 249 + 250 + clear(): void { 251 + this.fns = []; 252 + } 253 + 254 + eject(id: number | Interceptor): void { 255 + const index = this.getInterceptorIndex(id); 256 + if (this.fns[index]) { 257 + this.fns[index] = null; 258 + } 259 + } 260 + 261 + exists(id: number | Interceptor): boolean { 262 + const index = this.getInterceptorIndex(id); 263 + return Boolean(this.fns[index]); 264 + } 265 + 266 + getInterceptorIndex(id: number | Interceptor): number { 267 + if (typeof id === 'number') { 268 + return this.fns[id] ? id : -1; 269 + } 270 + return this.fns.indexOf(id); 271 + } 272 + 273 + update( 274 + id: number | Interceptor, 275 + fn: Interceptor, 276 + ): number | Interceptor | false { 277 + const index = this.getInterceptorIndex(id); 278 + if (this.fns[index]) { 279 + this.fns[index] = fn; 280 + return id; 281 + } 282 + return false; 283 + } 284 + 285 + use(fn: Interceptor): number { 286 + this.fns.push(fn); 287 + return this.fns.length - 1; 288 + } 289 + } 290 + 291 + export interface Middleware<Req, Res, Err, Options> { 292 + error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>; 293 + request: Interceptors<ReqInterceptor<Req, Options>>; 294 + response: Interceptors<ResInterceptor<Res, Req, Options>>; 295 + } 296 + 297 + export const createInterceptors = <Req, Res, Err, Options>(): Middleware< 298 + Req, 299 + Res, 300 + Err, 301 + Options 302 + > => ({ 303 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 304 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 305 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 306 + }); 307 + 308 + const defaultQuerySerializer = createQuerySerializer({ 309 + allowReserved: false, 310 + array: { 311 + explode: true, 312 + style: 'form', 313 + }, 314 + object: { 315 + explode: true, 316 + style: 'deepObject', 317 + }, 318 + }); 319 + 320 + const defaultHeaders = { 321 + 'Content-Type': 'application/json', 322 + }; 323 + 324 + export const createConfig = <T extends ClientOptions = ClientOptions>( 325 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 326 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 327 + ...jsonBodySerializer, 328 + headers: defaultHeaders, 329 + parseAs: 'auto', 330 + querySerializer: defaultQuerySerializer, 331 + ...override, 332 + });
+42
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/core/auth.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type AuthToken = string | undefined; 4 + 5 + export interface Auth { 6 + /** 7 + * Which part of the request do we use to send the auth? 8 + * 9 + * @default 'header' 10 + */ 11 + in?: 'header' | 'query' | 'cookie'; 12 + /** 13 + * Header or query parameter name. 14 + * 15 + * @default 'Authorization' 16 + */ 17 + name?: string; 18 + scheme?: 'basic' | 'bearer'; 19 + type: 'apiKey' | 'http'; 20 + } 21 + 22 + export const getAuthToken = async ( 23 + auth: Auth, 24 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 25 + ): Promise<string | undefined> => { 26 + const token = 27 + typeof callback === 'function' ? await callback(auth) : callback; 28 + 29 + if (!token) { 30 + return; 31 + } 32 + 33 + if (auth.scheme === 'bearer') { 34 + return `Bearer ${token}`; 35 + } 36 + 37 + if (auth.scheme === 'basic') { 38 + return `Basic ${btoa(token)}`; 39 + } 40 + 41 + return token; 42 + };
+100
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/core/bodySerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { 4 + ArrayStyle, 5 + ObjectStyle, 6 + SerializerOptions, 7 + } from './pathSerializer.gen'; 8 + 9 + export type QuerySerializer = (query: Record<string, unknown>) => string; 10 + 11 + export type BodySerializer = (body: any) => any; 12 + 13 + type QuerySerializerOptionsObject = { 14 + allowReserved?: boolean; 15 + array?: Partial<SerializerOptions<ArrayStyle>>; 16 + object?: Partial<SerializerOptions<ObjectStyle>>; 17 + }; 18 + 19 + export type QuerySerializerOptions = QuerySerializerOptionsObject & { 20 + /** 21 + * Per-parameter serialization overrides. When provided, these settings 22 + * override the global array/object settings for specific parameter names. 23 + */ 24 + parameters?: Record<string, QuerySerializerOptionsObject>; 25 + }; 26 + 27 + const serializeFormDataPair = ( 28 + data: FormData, 29 + key: string, 30 + value: unknown, 31 + ): void => { 32 + if (typeof value === 'string' || value instanceof Blob) { 33 + data.append(key, value); 34 + } else if (value instanceof Date) { 35 + data.append(key, value.toISOString()); 36 + } else { 37 + data.append(key, JSON.stringify(value)); 38 + } 39 + }; 40 + 41 + const serializeUrlSearchParamsPair = ( 42 + data: URLSearchParams, 43 + key: string, 44 + value: unknown, 45 + ): void => { 46 + if (typeof value === 'string') { 47 + data.append(key, value); 48 + } else { 49 + data.append(key, JSON.stringify(value)); 50 + } 51 + }; 52 + 53 + export const formDataBodySerializer = { 54 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 55 + body: T, 56 + ): FormData => { 57 + const data = new FormData(); 58 + 59 + Object.entries(body).forEach(([key, value]) => { 60 + if (value === undefined || value === null) { 61 + return; 62 + } 63 + if (Array.isArray(value)) { 64 + value.forEach((v) => serializeFormDataPair(data, key, v)); 65 + } else { 66 + serializeFormDataPair(data, key, value); 67 + } 68 + }); 69 + 70 + return data; 71 + }, 72 + }; 73 + 74 + export const jsonBodySerializer = { 75 + bodySerializer: <T>(body: T): string => 76 + JSON.stringify(body, (_key, value) => 77 + typeof value === 'bigint' ? value.toString() : value, 78 + ), 79 + }; 80 + 81 + export const urlSearchParamsBodySerializer = { 82 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 83 + body: T, 84 + ): string => { 85 + const data = new URLSearchParams(); 86 + 87 + Object.entries(body).forEach(([key, value]) => { 88 + if (value === undefined || value === null) { 89 + return; 90 + } 91 + if (Array.isArray(value)) { 92 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 93 + } else { 94 + serializeUrlSearchParamsPair(data, key, value); 95 + } 96 + }); 97 + 98 + return data.toString(); 99 + }, 100 + };
+153
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/core/params.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + type Slot = 'body' | 'headers' | 'path' | 'query'; 4 + 5 + export type Field = 6 + | { 7 + in: Exclude<Slot, 'body'>; 8 + /** 9 + * Field name. This is the name we want the user to see and use. 10 + */ 11 + key: string; 12 + /** 13 + * Field mapped name. This is the name we want to use in the request. 14 + * If omitted, we use the same value as `key`. 15 + */ 16 + map?: string; 17 + } 18 + | { 19 + in: Extract<Slot, 'body'>; 20 + /** 21 + * Key isn't required for bodies. 22 + */ 23 + key?: string; 24 + map?: string; 25 + }; 26 + 27 + export interface Fields { 28 + allowExtra?: Partial<Record<Slot, boolean>>; 29 + args?: ReadonlyArray<Field>; 30 + } 31 + 32 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 33 + 34 + const extraPrefixesMap: Record<string, Slot> = { 35 + $body_: 'body', 36 + $headers_: 'headers', 37 + $path_: 'path', 38 + $query_: 'query', 39 + }; 40 + const extraPrefixes = Object.entries(extraPrefixesMap); 41 + 42 + type KeyMap = Map< 43 + string, 44 + { 45 + in: Slot; 46 + map?: string; 47 + } 48 + >; 49 + 50 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 51 + if (!map) { 52 + map = new Map(); 53 + } 54 + 55 + for (const config of fields) { 56 + if ('in' in config) { 57 + if (config.key) { 58 + map.set(config.key, { 59 + in: config.in, 60 + map: config.map, 61 + }); 62 + } 63 + } else if (config.args) { 64 + buildKeyMap(config.args, map); 65 + } 66 + } 67 + 68 + return map; 69 + }; 70 + 71 + interface Params { 72 + body: unknown; 73 + headers: Record<string, unknown>; 74 + path: Record<string, unknown>; 75 + query: Record<string, unknown>; 76 + } 77 + 78 + const stripEmptySlots = (params: Params) => { 79 + for (const [slot, value] of Object.entries(params)) { 80 + if (value && typeof value === 'object' && !Object.keys(value).length) { 81 + delete params[slot as Slot]; 82 + } 83 + } 84 + }; 85 + 86 + export const buildClientParams = ( 87 + args: ReadonlyArray<unknown>, 88 + fields: FieldsConfig, 89 + ) => { 90 + const params: Params = { 91 + body: {}, 92 + headers: {}, 93 + path: {}, 94 + query: {}, 95 + }; 96 + 97 + const map = buildKeyMap(fields); 98 + 99 + let config: FieldsConfig[number] | undefined; 100 + 101 + for (const [index, arg] of args.entries()) { 102 + if (fields[index]) { 103 + config = fields[index]; 104 + } 105 + 106 + if (!config) { 107 + continue; 108 + } 109 + 110 + if ('in' in config) { 111 + if (config.key) { 112 + const field = map.get(config.key)!; 113 + const name = field.map || config.key; 114 + (params[field.in] as Record<string, unknown>)[name] = arg; 115 + } else { 116 + params.body = arg; 117 + } 118 + } else { 119 + for (const [key, value] of Object.entries(arg ?? {})) { 120 + const field = map.get(key); 121 + 122 + if (field) { 123 + const name = field.map || key; 124 + (params[field.in] as Record<string, unknown>)[name] = value; 125 + } else { 126 + const extra = extraPrefixes.find(([prefix]) => 127 + key.startsWith(prefix), 128 + ); 129 + 130 + if (extra) { 131 + const [prefix, slot] = extra; 132 + (params[slot] as Record<string, unknown>)[ 133 + key.slice(prefix.length) 134 + ] = value; 135 + } else { 136 + for (const [slot, allowed] of Object.entries( 137 + config.allowExtra ?? {}, 138 + )) { 139 + if (allowed) { 140 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 141 + break; 142 + } 143 + } 144 + } 145 + } 146 + } 147 + } 148 + } 149 + 150 + stripEmptySlots(params); 151 + 152 + return params; 153 + };
+181
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/core/pathSerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + interface SerializeOptions<T> 4 + extends SerializePrimitiveOptions, 5 + SerializerOptions<T> {} 6 + 7 + interface SerializePrimitiveOptions { 8 + allowReserved?: boolean; 9 + name: string; 10 + } 11 + 12 + export interface SerializerOptions<T> { 13 + /** 14 + * @default true 15 + */ 16 + explode: boolean; 17 + style: T; 18 + } 19 + 20 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 21 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 22 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 23 + export type ObjectStyle = 'form' | 'deepObject'; 24 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 25 + 26 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 27 + value: string; 28 + } 29 + 30 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 31 + switch (style) { 32 + case 'label': 33 + return '.'; 34 + case 'matrix': 35 + return ';'; 36 + case 'simple': 37 + return ','; 38 + default: 39 + return '&'; 40 + } 41 + }; 42 + 43 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 44 + switch (style) { 45 + case 'form': 46 + return ','; 47 + case 'pipeDelimited': 48 + return '|'; 49 + case 'spaceDelimited': 50 + return '%20'; 51 + default: 52 + return ','; 53 + } 54 + }; 55 + 56 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 57 + switch (style) { 58 + case 'label': 59 + return '.'; 60 + case 'matrix': 61 + return ';'; 62 + case 'simple': 63 + return ','; 64 + default: 65 + return '&'; 66 + } 67 + }; 68 + 69 + export const serializeArrayParam = ({ 70 + allowReserved, 71 + explode, 72 + name, 73 + style, 74 + value, 75 + }: SerializeOptions<ArraySeparatorStyle> & { 76 + value: unknown[]; 77 + }) => { 78 + if (!explode) { 79 + const joinedValues = ( 80 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 81 + ).join(separatorArrayNoExplode(style)); 82 + switch (style) { 83 + case 'label': 84 + return `.${joinedValues}`; 85 + case 'matrix': 86 + return `;${name}=${joinedValues}`; 87 + case 'simple': 88 + return joinedValues; 89 + default: 90 + return `${name}=${joinedValues}`; 91 + } 92 + } 93 + 94 + const separator = separatorArrayExplode(style); 95 + const joinedValues = value 96 + .map((v) => { 97 + if (style === 'label' || style === 'simple') { 98 + return allowReserved ? v : encodeURIComponent(v as string); 99 + } 100 + 101 + return serializePrimitiveParam({ 102 + allowReserved, 103 + name, 104 + value: v as string, 105 + }); 106 + }) 107 + .join(separator); 108 + return style === 'label' || style === 'matrix' 109 + ? separator + joinedValues 110 + : joinedValues; 111 + }; 112 + 113 + export const serializePrimitiveParam = ({ 114 + allowReserved, 115 + name, 116 + value, 117 + }: SerializePrimitiveParam) => { 118 + if (value === undefined || value === null) { 119 + return ''; 120 + } 121 + 122 + if (typeof value === 'object') { 123 + throw new Error( 124 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 125 + ); 126 + } 127 + 128 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 129 + }; 130 + 131 + export const serializeObjectParam = ({ 132 + allowReserved, 133 + explode, 134 + name, 135 + style, 136 + value, 137 + valueOnly, 138 + }: SerializeOptions<ObjectSeparatorStyle> & { 139 + value: Record<string, unknown> | Date; 140 + valueOnly?: boolean; 141 + }) => { 142 + if (value instanceof Date) { 143 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 144 + } 145 + 146 + if (style !== 'deepObject' && !explode) { 147 + let values: string[] = []; 148 + Object.entries(value).forEach(([key, v]) => { 149 + values = [ 150 + ...values, 151 + key, 152 + allowReserved ? (v as string) : encodeURIComponent(v as string), 153 + ]; 154 + }); 155 + const joinedValues = values.join(','); 156 + switch (style) { 157 + case 'form': 158 + return `${name}=${joinedValues}`; 159 + case 'label': 160 + return `.${joinedValues}`; 161 + case 'matrix': 162 + return `;${name}=${joinedValues}`; 163 + default: 164 + return joinedValues; 165 + } 166 + } 167 + 168 + const separator = separatorObjectExplode(style); 169 + const joinedValues = Object.entries(value) 170 + .map(([key, v]) => 171 + serializePrimitiveParam({ 172 + allowReserved, 173 + name: style === 'deepObject' ? `${name}[${key}]` : key, 174 + value: v as string, 175 + }), 176 + ) 177 + .join(separator); 178 + return style === 'label' || style === 'matrix' 179 + ? separator + joinedValues 180 + : joinedValues; 181 + };
+136
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/core/queryKeySerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + /** 4 + * JSON-friendly union that mirrors what Pinia Colada can hash. 5 + */ 6 + export type JsonValue = 7 + | null 8 + | string 9 + | number 10 + | boolean 11 + | JsonValue[] 12 + | { [key: string]: JsonValue }; 13 + 14 + /** 15 + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. 16 + */ 17 + export const queryKeyJsonReplacer = (_key: string, value: unknown) => { 18 + if ( 19 + value === undefined || 20 + typeof value === 'function' || 21 + typeof value === 'symbol' 22 + ) { 23 + return undefined; 24 + } 25 + if (typeof value === 'bigint') { 26 + return value.toString(); 27 + } 28 + if (value instanceof Date) { 29 + return value.toISOString(); 30 + } 31 + return value; 32 + }; 33 + 34 + /** 35 + * Safely stringifies a value and parses it back into a JsonValue. 36 + */ 37 + export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { 38 + try { 39 + const json = JSON.stringify(input, queryKeyJsonReplacer); 40 + if (json === undefined) { 41 + return undefined; 42 + } 43 + return JSON.parse(json) as JsonValue; 44 + } catch { 45 + return undefined; 46 + } 47 + }; 48 + 49 + /** 50 + * Detects plain objects (including objects with a null prototype). 51 + */ 52 + const isPlainObject = (value: unknown): value is Record<string, unknown> => { 53 + if (value === null || typeof value !== 'object') { 54 + return false; 55 + } 56 + const prototype = Object.getPrototypeOf(value as object); 57 + return prototype === Object.prototype || prototype === null; 58 + }; 59 + 60 + /** 61 + * Turns URLSearchParams into a sorted JSON object for deterministic keys. 62 + */ 63 + const serializeSearchParams = (params: URLSearchParams): JsonValue => { 64 + const entries = Array.from(params.entries()).sort(([a], [b]) => 65 + a.localeCompare(b), 66 + ); 67 + const result: Record<string, JsonValue> = {}; 68 + 69 + for (const [key, value] of entries) { 70 + const existing = result[key]; 71 + if (existing === undefined) { 72 + result[key] = value; 73 + continue; 74 + } 75 + 76 + if (Array.isArray(existing)) { 77 + (existing as string[]).push(value); 78 + } else { 79 + result[key] = [existing, value]; 80 + } 81 + } 82 + 83 + return result; 84 + }; 85 + 86 + /** 87 + * Normalizes any accepted value into a JSON-friendly shape for query keys. 88 + */ 89 + export const serializeQueryKeyValue = ( 90 + value: unknown, 91 + ): JsonValue | undefined => { 92 + if (value === null) { 93 + return null; 94 + } 95 + 96 + if ( 97 + typeof value === 'string' || 98 + typeof value === 'number' || 99 + typeof value === 'boolean' 100 + ) { 101 + return value; 102 + } 103 + 104 + if ( 105 + value === undefined || 106 + typeof value === 'function' || 107 + typeof value === 'symbol' 108 + ) { 109 + return undefined; 110 + } 111 + 112 + if (typeof value === 'bigint') { 113 + return value.toString(); 114 + } 115 + 116 + if (value instanceof Date) { 117 + return value.toISOString(); 118 + } 119 + 120 + if (Array.isArray(value)) { 121 + return stringifyToJsonValue(value); 122 + } 123 + 124 + if ( 125 + typeof URLSearchParams !== 'undefined' && 126 + value instanceof URLSearchParams 127 + ) { 128 + return serializeSearchParams(value); 129 + } 130 + 131 + if (isPlainObject(value)) { 132 + return stringifyToJsonValue(value); 133 + } 134 + 135 + return undefined; 136 + };
+264
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/core/serverSentEvents.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Config } from './types.gen'; 4 + 5 + export type ServerSentEventsOptions<TData = unknown> = Omit< 6 + RequestInit, 7 + 'method' 8 + > & 9 + Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & { 10 + /** 11 + * Fetch API implementation. You can use this option to provide a custom 12 + * fetch instance. 13 + * 14 + * @default globalThis.fetch 15 + */ 16 + fetch?: typeof fetch; 17 + /** 18 + * Implementing clients can call request interceptors inside this hook. 19 + */ 20 + onRequest?: (url: string, init: RequestInit) => Promise<Request>; 21 + /** 22 + * Callback invoked when a network or parsing error occurs during streaming. 23 + * 24 + * This option applies only if the endpoint returns a stream of events. 25 + * 26 + * @param error The error that occurred. 27 + */ 28 + onSseError?: (error: unknown) => void; 29 + /** 30 + * Callback invoked when an event is streamed from the server. 31 + * 32 + * This option applies only if the endpoint returns a stream of events. 33 + * 34 + * @param event Event streamed from the server. 35 + * @returns Nothing (void). 36 + */ 37 + onSseEvent?: (event: StreamEvent<TData>) => void; 38 + serializedBody?: RequestInit['body']; 39 + /** 40 + * Default retry delay in milliseconds. 41 + * 42 + * This option applies only if the endpoint returns a stream of events. 43 + * 44 + * @default 3000 45 + */ 46 + sseDefaultRetryDelay?: number; 47 + /** 48 + * Maximum number of retry attempts before giving up. 49 + */ 50 + sseMaxRetryAttempts?: number; 51 + /** 52 + * Maximum retry delay in milliseconds. 53 + * 54 + * Applies only when exponential backoff is used. 55 + * 56 + * This option applies only if the endpoint returns a stream of events. 57 + * 58 + * @default 30000 59 + */ 60 + sseMaxRetryDelay?: number; 61 + /** 62 + * Optional sleep function for retry backoff. 63 + * 64 + * Defaults to using `setTimeout`. 65 + */ 66 + sseSleepFn?: (ms: number) => Promise<void>; 67 + url: string; 68 + }; 69 + 70 + export interface StreamEvent<TData = unknown> { 71 + data: TData; 72 + event?: string; 73 + id?: string; 74 + retry?: number; 75 + } 76 + 77 + export type ServerSentEventsResult< 78 + TData = unknown, 79 + TReturn = void, 80 + TNext = unknown, 81 + > = { 82 + stream: AsyncGenerator< 83 + TData extends Record<string, unknown> ? TData[keyof TData] : TData, 84 + TReturn, 85 + TNext 86 + >; 87 + }; 88 + 89 + export const createSseClient = <TData = unknown>({ 90 + onRequest, 91 + onSseError, 92 + onSseEvent, 93 + responseTransformer, 94 + responseValidator, 95 + sseDefaultRetryDelay, 96 + sseMaxRetryAttempts, 97 + sseMaxRetryDelay, 98 + sseSleepFn, 99 + url, 100 + ...options 101 + }: ServerSentEventsOptions): ServerSentEventsResult<TData> => { 102 + let lastEventId: string | undefined; 103 + 104 + const sleep = 105 + sseSleepFn ?? 106 + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); 107 + 108 + const createStream = async function* () { 109 + let retryDelay: number = sseDefaultRetryDelay ?? 3000; 110 + let attempt = 0; 111 + const signal = options.signal ?? new AbortController().signal; 112 + 113 + while (true) { 114 + if (signal.aborted) break; 115 + 116 + attempt++; 117 + 118 + const headers = 119 + options.headers instanceof Headers 120 + ? options.headers 121 + : new Headers(options.headers as Record<string, string> | undefined); 122 + 123 + if (lastEventId !== undefined) { 124 + headers.set('Last-Event-ID', lastEventId); 125 + } 126 + 127 + try { 128 + const requestInit: RequestInit = { 129 + redirect: 'follow', 130 + ...options, 131 + body: options.serializedBody, 132 + headers, 133 + signal, 134 + }; 135 + let request = new Request(url, requestInit); 136 + if (onRequest) { 137 + request = await onRequest(url, requestInit); 138 + } 139 + // fetch must be assigned here, otherwise it would throw the error: 140 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 141 + const _fetch = options.fetch ?? globalThis.fetch; 142 + const response = await _fetch(request); 143 + 144 + if (!response.ok) 145 + throw new Error( 146 + `SSE failed: ${response.status} ${response.statusText}`, 147 + ); 148 + 149 + if (!response.body) throw new Error('No body in SSE response'); 150 + 151 + const reader = response.body 152 + .pipeThrough(new TextDecoderStream()) 153 + .getReader(); 154 + 155 + let buffer = ''; 156 + 157 + const abortHandler = () => { 158 + try { 159 + reader.cancel(); 160 + } catch { 161 + // noop 162 + } 163 + }; 164 + 165 + signal.addEventListener('abort', abortHandler); 166 + 167 + try { 168 + while (true) { 169 + const { done, value } = await reader.read(); 170 + if (done) break; 171 + buffer += value; 172 + 173 + const chunks = buffer.split('\n\n'); 174 + buffer = chunks.pop() ?? ''; 175 + 176 + for (const chunk of chunks) { 177 + const lines = chunk.split('\n'); 178 + const dataLines: Array<string> = []; 179 + let eventName: string | undefined; 180 + 181 + for (const line of lines) { 182 + if (line.startsWith('data:')) { 183 + dataLines.push(line.replace(/^data:\s*/, '')); 184 + } else if (line.startsWith('event:')) { 185 + eventName = line.replace(/^event:\s*/, ''); 186 + } else if (line.startsWith('id:')) { 187 + lastEventId = line.replace(/^id:\s*/, ''); 188 + } else if (line.startsWith('retry:')) { 189 + const parsed = Number.parseInt( 190 + line.replace(/^retry:\s*/, ''), 191 + 10, 192 + ); 193 + if (!Number.isNaN(parsed)) { 194 + retryDelay = parsed; 195 + } 196 + } 197 + } 198 + 199 + let data: unknown; 200 + let parsedJson = false; 201 + 202 + if (dataLines.length) { 203 + const rawData = dataLines.join('\n'); 204 + try { 205 + data = JSON.parse(rawData); 206 + parsedJson = true; 207 + } catch { 208 + data = rawData; 209 + } 210 + } 211 + 212 + if (parsedJson) { 213 + if (responseValidator) { 214 + await responseValidator(data); 215 + } 216 + 217 + if (responseTransformer) { 218 + data = await responseTransformer(data); 219 + } 220 + } 221 + 222 + onSseEvent?.({ 223 + data, 224 + event: eventName, 225 + id: lastEventId, 226 + retry: retryDelay, 227 + }); 228 + 229 + if (dataLines.length) { 230 + yield data as any; 231 + } 232 + } 233 + } 234 + } finally { 235 + signal.removeEventListener('abort', abortHandler); 236 + reader.releaseLock(); 237 + } 238 + 239 + break; // exit loop on normal completion 240 + } catch (error) { 241 + // connection failed or aborted; retry after delay 242 + onSseError?.(error); 243 + 244 + if ( 245 + sseMaxRetryAttempts !== undefined && 246 + attempt >= sseMaxRetryAttempts 247 + ) { 248 + break; // stop after firing error 249 + } 250 + 251 + // exponential backoff: double retry each attempt, cap at 30s 252 + const backoff = Math.min( 253 + retryDelay * 2 ** (attempt - 1), 254 + sseMaxRetryDelay ?? 30000, 255 + ); 256 + await sleep(backoff); 257 + } 258 + } 259 + }; 260 + 261 + const stream = createStream(); 262 + 263 + return { stream }; 264 + };
+118
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/core/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Auth, AuthToken } from './auth.gen'; 4 + import type { 5 + BodySerializer, 6 + QuerySerializer, 7 + QuerySerializerOptions, 8 + } from './bodySerializer.gen'; 9 + 10 + export type HttpMethod = 11 + | 'connect' 12 + | 'delete' 13 + | 'get' 14 + | 'head' 15 + | 'options' 16 + | 'patch' 17 + | 'post' 18 + | 'put' 19 + | 'trace'; 20 + 21 + export type Client< 22 + RequestFn = never, 23 + Config = unknown, 24 + MethodFn = never, 25 + BuildUrlFn = never, 26 + SseFn = never, 27 + > = { 28 + /** 29 + * Returns the final request URL. 30 + */ 31 + buildUrl: BuildUrlFn; 32 + getConfig: () => Config; 33 + request: RequestFn; 34 + setConfig: (config: Config) => Config; 35 + } & { 36 + [K in HttpMethod]: MethodFn; 37 + } & ([SseFn] extends [never] 38 + ? { sse?: never } 39 + : { sse: { [K in HttpMethod]: SseFn } }); 40 + 41 + export interface Config { 42 + /** 43 + * Auth token or a function returning auth token. The resolved value will be 44 + * added to the request payload as defined by its `security` array. 45 + */ 46 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 47 + /** 48 + * A function for serializing request body parameter. By default, 49 + * {@link JSON.stringify()} will be used. 50 + */ 51 + bodySerializer?: BodySerializer | null; 52 + /** 53 + * An object containing any HTTP headers that you want to pre-populate your 54 + * `Headers` object with. 55 + * 56 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 57 + */ 58 + headers?: 59 + | RequestInit['headers'] 60 + | Record< 61 + string, 62 + | string 63 + | number 64 + | boolean 65 + | (string | number | boolean)[] 66 + | null 67 + | undefined 68 + | unknown 69 + >; 70 + /** 71 + * The request method. 72 + * 73 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 74 + */ 75 + method?: Uppercase<HttpMethod>; 76 + /** 77 + * A function for serializing request query parameters. By default, arrays 78 + * will be exploded in form style, objects will be exploded in deepObject 79 + * style, and reserved characters are percent-encoded. 80 + * 81 + * This method will have no effect if the native `paramsSerializer()` Axios 82 + * API function is used. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 87 + /** 88 + * A function validating request data. This is useful if you want to ensure 89 + * the request conforms to the desired shape, so it can be safely sent to 90 + * the server. 91 + */ 92 + requestValidator?: (data: unknown) => Promise<unknown>; 93 + /** 94 + * A function transforming response data before it's returned. This is useful 95 + * for post-processing data, e.g. converting ISO strings into Date objects. 96 + */ 97 + responseTransformer?: (data: unknown) => Promise<unknown>; 98 + /** 99 + * A function validating response data. This is useful if you want to ensure 100 + * the response conforms to the desired shape, so it can be safely passed to 101 + * the transformers and returned to the user. 102 + */ 103 + responseValidator?: (data: unknown) => Promise<unknown>; 104 + } 105 + 106 + type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never] 107 + ? true 108 + : [T] extends [never | undefined] 109 + ? [undefined] extends [T] 110 + ? false 111 + : true 112 + : false; 113 + 114 + export type OmitNever<T extends Record<string, unknown>> = { 115 + [K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true 116 + ? never 117 + : K]: T[K]; 118 + };
+143
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/core/utils.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; 4 + import { 5 + type ArraySeparatorStyle, 6 + serializeArrayParam, 7 + serializeObjectParam, 8 + serializePrimitiveParam, 9 + } from './pathSerializer.gen'; 10 + 11 + export interface PathSerializer { 12 + path: Record<string, unknown>; 13 + url: string; 14 + } 15 + 16 + export const PATH_PARAM_RE = /\{[^{}]+\}/g; 17 + 18 + export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 19 + let url = _url; 20 + const matches = _url.match(PATH_PARAM_RE); 21 + if (matches) { 22 + for (const match of matches) { 23 + let explode = false; 24 + let name = match.substring(1, match.length - 1); 25 + let style: ArraySeparatorStyle = 'simple'; 26 + 27 + if (name.endsWith('*')) { 28 + explode = true; 29 + name = name.substring(0, name.length - 1); 30 + } 31 + 32 + if (name.startsWith('.')) { 33 + name = name.substring(1); 34 + style = 'label'; 35 + } else if (name.startsWith(';')) { 36 + name = name.substring(1); 37 + style = 'matrix'; 38 + } 39 + 40 + const value = path[name]; 41 + 42 + if (value === undefined || value === null) { 43 + continue; 44 + } 45 + 46 + if (Array.isArray(value)) { 47 + url = url.replace( 48 + match, 49 + serializeArrayParam({ explode, name, style, value }), 50 + ); 51 + continue; 52 + } 53 + 54 + if (typeof value === 'object') { 55 + url = url.replace( 56 + match, 57 + serializeObjectParam({ 58 + explode, 59 + name, 60 + style, 61 + value: value as Record<string, unknown>, 62 + valueOnly: true, 63 + }), 64 + ); 65 + continue; 66 + } 67 + 68 + if (style === 'matrix') { 69 + url = url.replace( 70 + match, 71 + `;${serializePrimitiveParam({ 72 + name, 73 + value: value as string, 74 + })}`, 75 + ); 76 + continue; 77 + } 78 + 79 + const replaceValue = encodeURIComponent( 80 + style === 'label' ? `.${value as string}` : (value as string), 81 + ); 82 + url = url.replace(match, replaceValue); 83 + } 84 + } 85 + return url; 86 + }; 87 + 88 + export const getUrl = ({ 89 + baseUrl, 90 + path, 91 + query, 92 + querySerializer, 93 + url: _url, 94 + }: { 95 + baseUrl?: string; 96 + path?: Record<string, unknown>; 97 + query?: Record<string, unknown>; 98 + querySerializer: QuerySerializer; 99 + url: string; 100 + }) => { 101 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 102 + let url = (baseUrl ?? '') + pathUrl; 103 + if (path) { 104 + url = defaultPathSerializer({ path, url }); 105 + } 106 + let search = query ? querySerializer(query) : ''; 107 + if (search.startsWith('?')) { 108 + search = search.substring(1); 109 + } 110 + if (search) { 111 + url += `?${search}`; 112 + } 113 + return url; 114 + }; 115 + 116 + export function getValidRequestBody(options: { 117 + body?: unknown; 118 + bodySerializer?: BodySerializer | null; 119 + serializedBody?: unknown; 120 + }) { 121 + const hasBody = options.body !== undefined; 122 + const isSerializedBody = hasBody && options.bodySerializer; 123 + 124 + if (isSerializedBody) { 125 + if ('serializedBody' in options) { 126 + const hasSerializedBody = 127 + options.serializedBody !== undefined && options.serializedBody !== ''; 128 + 129 + return hasSerializedBody ? options.serializedBody : null; 130 + } 131 + 132 + // not all clients implement a serializedBody property (i.e. client-axios) 133 + return options.body !== '' ? options.body : null; 134 + } 135 + 136 + // plain/text body 137 + if (hasBody) { 138 + return options.body; 139 + } 140 + 141 + // no body was provided 142 + return undefined; 143 + }
+3
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type * from './types.gen';
+22
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/transformers.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { GetTreeResponse } from './types.gen'; 4 + 5 + const treeNodeSchemaResponseTransformer = (data: any) => { 6 + if (data.createdAt) { 7 + data.createdAt = new Date(data.createdAt); 8 + } 9 + if (data.children) { 10 + data.children = data.children.map((item: any) => { 11 + return treeNodeSchemaResponseTransformer(item); 12 + }); 13 + } 14 + return data; 15 + }; 16 + 17 + export const getTreeResponseTransformer = async (data: any): Promise<GetTreeResponse> => { 18 + data = data.map((item: any) => { 19 + return treeNodeSchemaResponseTransformer(item); 20 + }); 21 + return data; 22 + };
+28
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-recursive/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type ClientOptions = { 4 + baseUrl: `${string}://${string}` | (string & {}); 5 + }; 6 + 7 + export type TreeNode = { 8 + id?: string; 9 + name?: string; 10 + createdAt?: Date; 11 + children?: Array<TreeNode>; 12 + }; 13 + 14 + export type GetTreeData = { 15 + body?: never; 16 + path?: never; 17 + query?: never; 18 + url: '/tree'; 19 + }; 20 + 21 + export type GetTreeResponses = { 22 + /** 23 + * Success 24 + */ 25 + 200: Array<TreeNode>; 26 + }; 27 + 28 + export type GetTreeResponse = GetTreeResponses[keyof GetTreeResponses];
+54
specs/3.0.x/transformers-recursive.json
··· 1 + { 2 + "openapi": "3.0.3", 3 + "info": { 4 + "title": "Recursive Schema Test", 5 + "version": "1.0.0" 6 + }, 7 + "paths": { 8 + "/tree": { 9 + "get": { 10 + "operationId": "getTree", 11 + "responses": { 12 + "200": { 13 + "description": "Success", 14 + "content": { 15 + "application/json": { 16 + "schema": { 17 + "type": "array", 18 + "items": { 19 + "$ref": "#/components/schemas/TreeNode" 20 + } 21 + } 22 + } 23 + } 24 + } 25 + } 26 + } 27 + } 28 + }, 29 + "components": { 30 + "schemas": { 31 + "TreeNode": { 32 + "type": "object", 33 + "properties": { 34 + "id": { 35 + "type": "string" 36 + }, 37 + "name": { 38 + "type": "string" 39 + }, 40 + "createdAt": { 41 + "type": "string", 42 + "format": "date-time" 43 + }, 44 + "children": { 45 + "type": "array", 46 + "items": { 47 + "$ref": "#/components/schemas/TreeNode" 48 + } 49 + } 50 + } 51 + } 52 + } 53 + } 54 + }
+54
specs/3.1.x/transformers-recursive.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { 4 + "title": "Recursive Schema Test", 5 + "version": "1.0.0" 6 + }, 7 + "paths": { 8 + "/tree": { 9 + "get": { 10 + "operationId": "getTree", 11 + "responses": { 12 + "200": { 13 + "description": "Success", 14 + "content": { 15 + "application/json": { 16 + "schema": { 17 + "type": "array", 18 + "items": { 19 + "$ref": "#/components/schemas/TreeNode" 20 + } 21 + } 22 + } 23 + } 24 + } 25 + } 26 + } 27 + } 28 + }, 29 + "components": { 30 + "schemas": { 31 + "TreeNode": { 32 + "type": "object", 33 + "properties": { 34 + "id": { 35 + "type": "string" 36 + }, 37 + "name": { 38 + "type": "string" 39 + }, 40 + "createdAt": { 41 + "type": "string", 42 + "format": "date-time" 43 + }, 44 + "children": { 45 + "type": "array", 46 + "items": { 47 + "$ref": "#/components/schemas/TreeNode" 48 + } 49 + } 50 + } 51 + } 52 + } 53 + } 54 + }