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

Configure Feed

Select the types of activity you want to include in your feed.

refactor: simplify transformers plugin logic

Lubos 7e4050b2 485af894

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