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.

Merge pull request #3409 from codercms/fix/transformers-additional-properties

fix(transformers): respect additionalProperties when generating object map transforms

authored by

Lubos and committed by
GitHub
ebb5832d 05f6fdf5

+2368 -30
+5
.changeset/sad-impalas-taste.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **plugin(@hey-api/transformers)**: fix: handle additional properties
+8
packages/openapi-ts-tests/main/test/3.1.x.test.ts
··· 898 898 }, 899 899 { 900 900 config: createConfig({ 901 + input: 'transformers-additional-properties.yaml', 902 + output: 'transformers-additional-properties', 903 + plugins: ['@hey-api/client-fetch', '@hey-api/transformers'], 904 + }), 905 + description: 'transforms additionalProperties map values', 906 + }, 907 + { 908 + config: createConfig({ 901 909 input: 'transformers-recursive.json', 902 910 output: 'transformers-recursive', 903 911 plugins: ['@hey-api/client-fetch', '@hey-api/transformers'],
+16
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-additional-properties/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/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/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/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/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/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/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/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/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/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/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/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/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/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses } from './types.gen';
+23
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-additional-properties/transformers.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { PostFooResponse } from './types.gen'; 4 + 5 + const fooSchemaResponseTransformer = (data: any) => { 6 + data.foo = new Date(data.foo); 7 + return data; 8 + }; 9 + 10 + const barSchemaResponseTransformer = (data: any) => { 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]); 15 + } 16 + } 17 + return data; 18 + }; 19 + 20 + export const postFooResponseTransformer = async (data: any): Promise<PostFooResponse> => { 21 + data = barSchemaResponseTransformer(data); 22 + return data; 23 + };
+32
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-additional-properties/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 Foo = { 8 + foo: Date; 9 + }; 10 + 11 + export type Bar = { 12 + bar: Date; 13 + baz?: { 14 + [key: string]: Foo; 15 + }; 16 + }; 17 + 18 + export type PostFooData = { 19 + body?: never; 20 + path?: never; 21 + query?: never; 22 + url: '/foo'; 23 + }; 24 + 25 + export type PostFooResponses = { 26 + /** 27 + * OK 28 + */ 29 + 200: Bar; 30 + }; 31 + 32 + export type PostFooResponse = PostFooResponses[keyof PostFooResponses];
+3 -3
packages/openapi-ts/src/plugins/@hey-api/transformers/expressions.ts
··· 8 8 9 9 const bigIntCallExpression = 10 10 dataExpression !== undefined 11 - ? $('BigInt').call($.expr(dataExpression).attr('toString').call()) 11 + ? $('BigInt').call($(dataExpression).attr('toString').call()) 12 12 : undefined; 13 13 14 14 if (bigIntCallExpression) { ··· 17 17 } 18 18 19 19 if (dataExpression) { 20 - return [$.expr(dataExpression).assign(bigIntCallExpression)]; 20 + return [$(dataExpression).assign(bigIntCallExpression)]; 21 21 } 22 22 } 23 23 ··· 34 34 } 35 35 36 36 if (dataExpression) { 37 - return [$.expr(dataExpression).assign($.new('Date').arg(dataExpression))]; 37 + return [$(dataExpression).assign($.new('Date').arg(dataExpression))]; 38 38 } 39 39 40 40 return;
+17 -1
packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts
··· 12 12 // can emit calls to transformers that will be implemented later. 13 13 const buildingSymbols = new Set<number>(); 14 14 15 - type Expr = ReturnType<typeof $.fromValue | typeof $.return | typeof $.if>; 15 + type Expr = ReturnType<typeof $.fromValue | typeof $.for | typeof $.if | typeof $.return>; 16 16 17 17 function isNodeReturnStatement(node: Expr) { 18 18 return node['~dsl'] === 'ReturnTsDsl'; ··· 190 190 // this place influences all underlying transformers, while it's not exactly transformer itself 191 191 // Keep in mind that !!0 === false, so it already makes output for Bigint undesirable 192 192 $.if(propertyAccessExpression).do(...propertyNodes), 193 + ); 194 + } 195 + } 196 + 197 + if (schema.additionalProperties && dataExpression) { 198 + const entryValueNodes = processSchemaType({ 199 + dataExpression: $(dataExpression).attr('key').computed(), 200 + plugin, 201 + schema: schema.additionalProperties, 202 + }); 203 + 204 + if (entryValueNodes.length) { 205 + nodes.push( 206 + $.for($.const('key')) 207 + .of($('Object').attr('keys').call(dataExpression)) 208 + .do(...entryValueNodes), 193 209 ); 194 210 } 195 211 }
+19 -15
packages/openapi-ts/src/ts-dsl/expr/attr.ts
··· 25 25 export class AttrTsDsl extends Mixed { 26 26 readonly '~dsl' = 'AttrTsDsl'; 27 27 28 - protected left: Ref<AttrLeft>; 28 + protected _computed = false; 29 + protected _left: Ref<AttrLeft>; 29 30 30 31 constructor(left: AttrLeft, right: NodeName) { 31 32 super(); 32 - this.left = ref(left); 33 + this._left = ref(left); 33 34 this.name.set(right); 34 35 } 35 36 36 37 override analyze(ctx: AnalysisContext): void { 37 38 super.analyze(ctx); 38 - ctx.analyze(this.left); 39 + ctx.analyze(this._left); 39 40 ctx.analyze(this.name); 40 41 } 41 42 43 + /** Use computed property access (e.g., `obj[expr]`)? */ 44 + computed(condition?: boolean): this { 45 + this._computed = condition ?? true; 46 + return this; 47 + } 48 + 42 49 override toAst() { 43 - const leftNode = this.$node(this.left); 50 + const left = this.$node(this._left); 44 51 regexp.typeScriptIdentifier.lastIndex = 0; 45 - const right = fromRef(this.name); 46 - if (!regexp.typeScriptIdentifier.test(this.name.toString())) { 52 + if (this._computed || !regexp.typeScriptIdentifier.test(this.name.toString())) { 53 + const right = fromRef(this.name); 47 54 let value = isSymbol(right) ? right.finalName : right; 48 55 if (typeof value === 'string') { 49 56 if ( ··· 55 62 } 56 63 if (this._optional) { 57 64 return ts.factory.createElementAccessChain( 58 - leftNode, 65 + left, 59 66 this.$node(new TokenTsDsl().questionDot()), 60 - this.$node(new LiteralTsDsl(value)), 67 + this.$node(this._computed ? this.name : new LiteralTsDsl(value)), 61 68 ); 62 69 } 63 70 return ts.factory.createElementAccessExpression( 64 - leftNode, 65 - this.$node(new LiteralTsDsl(value)), 71 + left, 72 + this.$node(this._computed ? this.name : new LiteralTsDsl(value)), 66 73 ); 67 74 } 68 75 if (this._optional) { 69 76 return ts.factory.createPropertyAccessChain( 70 - leftNode, 77 + left, 71 78 this.$node(new TokenTsDsl().questionDot()), 72 79 this.$node(this.name) as ts.MemberName, 73 80 ); 74 81 } 75 - return ts.factory.createPropertyAccessExpression( 76 - leftNode, 77 - this.$node(this.name) as ts.MemberName, 78 - ); 82 + return ts.factory.createPropertyAccessExpression(left, this.$node(this.name) as ts.MemberName); 79 83 } 80 84 } 81 85
+78
packages/openapi-ts/src/ts-dsl/expr/postfix.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + import ts from 'typescript'; 3 + 4 + import type { MaybeTsDsl } from '../base'; 5 + import { TsDsl } from '../base'; 6 + 7 + export type PostfixExpr = string | MaybeTsDsl<ts.Expression>; 8 + export type PostfixOp = ts.PostfixUnaryOperator; 9 + 10 + const Mixed = TsDsl<ts.PostfixUnaryExpression>; 11 + 12 + export class PostfixTsDsl extends Mixed { 13 + readonly '~dsl' = 'PostfixTsDsl'; 14 + 15 + protected _expr?: PostfixExpr; 16 + protected _op?: PostfixOp; 17 + 18 + constructor(expr?: PostfixExpr, op?: PostfixOp) { 19 + super(); 20 + this._expr = expr; 21 + this._op = op; 22 + } 23 + 24 + override analyze(ctx: AnalysisContext): void { 25 + super.analyze(ctx); 26 + ctx.analyze(this._expr); 27 + } 28 + 29 + /** Returns true when all required builder calls are present. */ 30 + get isValid(): boolean { 31 + return this.missingRequiredCalls().length === 0; 32 + } 33 + 34 + /** Sets the operator to MinusMinusToken for decrement (`--`). */ 35 + dec(): this { 36 + this._op = ts.SyntaxKind.MinusMinusToken; 37 + return this; 38 + } 39 + 40 + /** Sets the operand (the expression being postfixed). */ 41 + expr(expr: PostfixExpr): this { 42 + this._expr = expr; 43 + return this; 44 + } 45 + 46 + /** Sets the operator to PlusPlusToken for increment (`++`). */ 47 + inc(): this { 48 + this._op = ts.SyntaxKind.PlusPlusToken; 49 + return this; 50 + } 51 + 52 + /** Sets the operator (e.g., `ts.SyntaxKind.PlusPlusToken` for `++`). */ 53 + op(op: PostfixOp): this { 54 + this._op = op; 55 + return this; 56 + } 57 + 58 + override toAst() { 59 + this.$validate(); 60 + return ts.factory.createPostfixUnaryExpression(this.$node(this._expr), this._op); 61 + } 62 + 63 + $validate(): asserts this is this & { 64 + _expr: PostfixExpr; 65 + _op: PostfixOp; 66 + } { 67 + const missing = this.missingRequiredCalls(); 68 + if (missing.length === 0) return; 69 + throw new Error(`Postfix unary expression missing ${missing.join(' and ')}`); 70 + } 71 + 72 + private missingRequiredCalls(): ReadonlyArray<string> { 73 + const missing: Array<string> = []; 74 + if (!this._expr) missing.push('.expr()'); 75 + if (!this._op) missing.push('operator (e.g., .inc(), .dec())'); 76 + return missing; 77 + } 78 + }
+10 -7
packages/openapi-ts/src/ts-dsl/expr/prefix.ts
··· 4 4 import type { MaybeTsDsl } from '../base'; 5 5 import { TsDsl } from '../base'; 6 6 7 + export type PrefixExpr = string | MaybeTsDsl<ts.Expression>; 8 + export type PrefixOp = ts.PrefixUnaryOperator; 9 + 7 10 const Mixed = TsDsl<ts.PrefixUnaryExpression>; 8 11 9 12 export class PrefixTsDsl extends Mixed { 10 13 readonly '~dsl' = 'PrefixTsDsl'; 11 14 12 - protected _expr?: string | MaybeTsDsl<ts.Expression>; 13 - protected _op?: ts.PrefixUnaryOperator; 15 + protected _expr?: PrefixExpr; 16 + protected _op?: PrefixOp; 14 17 15 - constructor(expr?: string | MaybeTsDsl<ts.Expression>, op?: ts.PrefixUnaryOperator) { 18 + constructor(expr?: PrefixExpr, op?: PrefixOp) { 16 19 super(); 17 20 this._expr = expr; 18 21 this._op = op; ··· 29 32 } 30 33 31 34 /** Sets the operand (the expression being prefixed). */ 32 - expr(expr: string | MaybeTsDsl<ts.Expression>): this { 35 + expr(expr: PrefixExpr): this { 33 36 this._expr = expr; 34 37 return this; 35 38 } ··· 47 50 } 48 51 49 52 /** Sets the operator (e.g. `ts.SyntaxKind.ExclamationToken` for `!`). */ 50 - op(op: ts.PrefixUnaryOperator): this { 53 + op(op: PrefixOp): this { 51 54 this._op = op; 52 55 return this; 53 56 } ··· 58 61 } 59 62 60 63 $validate(): asserts this is this & { 61 - _expr: string | MaybeTsDsl<ts.Expression>; 62 - _op: ts.PrefixUnaryOperator; 64 + _expr: PrefixExpr; 65 + _op: PrefixOp; 63 66 } { 64 67 const missing = this.missingRequiredCalls(); 65 68 if (missing.length === 0) return;
+25 -2
packages/openapi-ts/src/ts-dsl/index.ts
··· 24 24 import { LiteralTsDsl } from './expr/literal'; 25 25 import { NewTsDsl } from './expr/new'; 26 26 import { ObjectTsDsl } from './expr/object'; 27 + import { PostfixTsDsl } from './expr/postfix'; 27 28 import { PrefixTsDsl } from './expr/prefix'; 28 29 import { ObjectPropTsDsl } from './expr/prop'; 29 30 import { RegExpTsDsl } from './expr/regexp'; ··· 35 36 import { NewlineTsDsl } from './layout/newline'; 36 37 import { NoteTsDsl } from './layout/note'; 37 38 import { BlockTsDsl } from './stmt/block'; 39 + import type { ForCondition, ForIterable, ForMode } from './stmt/for'; 40 + import { ForTsDsl } from './stmt/for'; 38 41 import { IfTsDsl } from './stmt/if'; 39 42 import { ReturnTsDsl } from './stmt/return'; 40 43 import { StmtTsDsl } from './stmt/stmt'; ··· 88 91 /** Creates a constant variable declaration (`const`). */ 89 92 const: (...args: ConstructorParameters<typeof VarTsDsl>) => new VarTsDsl(...args).const(), 90 93 94 + /** Creates a postfix decrement expression (`i--`). */ 95 + dec: (...args: ConstructorParameters<typeof PostfixTsDsl>) => new PostfixTsDsl(...args).dec(), 96 + 91 97 /** Creates a decorator expression (e.g. `@decorator`). */ 92 98 decorator: (...args: ConstructorParameters<typeof DecoratorTsDsl>) => new DecoratorTsDsl(...args), 93 99 ··· 103 109 /** Creates a field declaration in a class or object. */ 104 110 field: (...args: ConstructorParameters<typeof FieldTsDsl>) => new FieldTsDsl(...args), 105 111 112 + /** Creates a for loop (for, for...of, for...in, or for await...of). */ 113 + for: ((...args: ReadonlyArray<any>) => new ForTsDsl(...args)) as { 114 + (variableOrInit?: VarTsDsl): ForTsDsl<ForMode>; 115 + ( 116 + variableOrInit: VarTsDsl, 117 + condition: ForCondition, 118 + iterableOrUpdate?: ForIterable, 119 + ): ForTsDsl<'for'>; 120 + <T extends ForMode>( 121 + variableOrInit: VarTsDsl, 122 + mode: T, 123 + iterableOrUpdate?: ForIterable, 124 + ): ForTsDsl<T>; 125 + }, 126 + 106 127 /** Converts a runtime value into a corresponding expression node. */ 107 128 fromValue: (...args: Parameters<typeof exprValue>) => exprValue(...args), 108 129 109 130 /** Creates a function expression or declaration. */ 110 131 func: ((nameOrFn?: any, fn?: any) => { 111 132 if (nameOrFn === undefined) return new FuncTsDsl(); 112 - if (typeof nameOrFn !== 'string') return new FuncTsDsl(nameOrFn); 113 - if (fn === undefined) return new FuncTsDsl(nameOrFn); 133 + if (typeof nameOrFn !== 'string' || fn === undefined) return new FuncTsDsl(nameOrFn); 114 134 return new FuncTsDsl(nameOrFn, fn); 115 135 }) as { 116 136 (): FuncTsDsl<'arrow'>; ··· 131 151 132 152 /** Creates an if statement. */ 133 153 if: (...args: ConstructorParameters<typeof IfTsDsl>) => new IfTsDsl(...args), 154 + 155 + /** Creates a postfix increment expression (`i++`). */ 156 + inc: (...args: ConstructorParameters<typeof PostfixTsDsl>) => new PostfixTsDsl(...args).inc(), 134 157 135 158 /** Creates an initialization block or statement. */ 136 159 init: (...args: ConstructorParameters<typeof InitTsDsl>) => new InitTsDsl(...args),
+179
packages/openapi-ts/src/ts-dsl/stmt/for.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + import ts from 'typescript'; 3 + 4 + import type { MaybeTsDsl } from '../base'; 5 + import { TsDsl } from '../base'; 6 + import { DoMixin } from '../mixins/do'; 7 + import { LayoutMixin } from '../mixins/layout'; 8 + import { BlockTsDsl } from './block'; 9 + import type { VarTsDsl } from './var'; 10 + 11 + export type ForMode = 'for' | 'in' | 'of'; 12 + export type ForCondition = MaybeTsDsl<ts.Expression>; 13 + export type ForIterable = MaybeTsDsl<ts.Expression>; 14 + 15 + const Mixed = DoMixin(LayoutMixin(TsDsl<ts.ForStatement>)); 16 + 17 + class ImplForTsDsl<M extends ForMode = 'for'> extends Mixed { 18 + readonly '~dsl' = 'ForTsDsl'; 19 + 20 + protected _await?: boolean; 21 + protected _condition?: ForCondition; 22 + protected _iterableOrUpdate?: ForIterable; 23 + protected _mode: ForMode = 'for'; 24 + protected _variableOrInit?: VarTsDsl; 25 + 26 + constructor(); 27 + constructor( 28 + variableOrInit?: VarTsDsl, 29 + modeOrCondition?: ForMode | ForCondition, 30 + iterableOrUpdate?: ForIterable, 31 + ) { 32 + super(); 33 + this._iterableOrUpdate = iterableOrUpdate; 34 + this._variableOrInit = variableOrInit; 35 + if (typeof modeOrCondition === 'string') { 36 + this._mode = modeOrCondition ?? 'for'; 37 + } else { 38 + this._condition = modeOrCondition; 39 + } 40 + } 41 + 42 + override analyze(ctx: AnalysisContext): void { 43 + ctx.analyze(this._condition); 44 + ctx.analyze(this._iterableOrUpdate); 45 + ctx.analyze(this._variableOrInit); 46 + ctx.pushScope(); 47 + try { 48 + super.analyze(ctx); 49 + } finally { 50 + ctx.popScope(); 51 + } 52 + } 53 + 54 + /** Returns true when all required builder calls are present. */ 55 + get isValid(): boolean { 56 + return this.missingRequiredCalls().length === 0; 57 + } 58 + 59 + /** Enables async iteration (`for await...of`). Can only be called on for...of. */ 60 + await(): ForTsDsl<'of'> { 61 + this._await = true; 62 + this.of(); 63 + return this as unknown as ForTsDsl<'of'>; 64 + } 65 + 66 + /** Sets the condition (e.g., `i < n`). */ 67 + condition(condition: ForCondition): this { 68 + this._condition = condition; 69 + return this; 70 + } 71 + 72 + /** Sets the iteration variable (e.g., `$.const('item')`). */ 73 + each(variable: VarTsDsl): this { 74 + this._variableOrInit = variable; 75 + return this; 76 + } 77 + 78 + /** Sets the object to iterate over and switches to for...in. */ 79 + in(iterable?: ForIterable): ForTsDsl<'in'> { 80 + this._mode = 'in'; 81 + if (iterable !== undefined) this.iterable(iterable); 82 + return this as unknown as ForTsDsl<'in'>; 83 + } 84 + 85 + /** Sets the initialization (e.g., `let i = 0`). */ 86 + init(init: VarTsDsl): this { 87 + this._variableOrInit = init; 88 + return this; 89 + } 90 + 91 + /** Sets the iterable to iterate over. */ 92 + iterable(iterable: ForIterable): this { 93 + this._iterableOrUpdate = iterable; 94 + return this; 95 + } 96 + 97 + /** Sets the iterable to iterate over and switches to for...of. */ 98 + of(iterable?: ForIterable): ForTsDsl<'of'> { 99 + this._mode = 'of'; 100 + if (iterable !== undefined) this.iterable(iterable); 101 + return this as unknown as ForTsDsl<'of'>; 102 + } 103 + 104 + /** Sets the update expression (e.g., `i++`). */ 105 + update(update: ForIterable): this { 106 + this._iterableOrUpdate = update; 107 + return this; 108 + } 109 + 110 + // @ts-expect-error --- need to fix types --- 111 + override toAst(): M extends 'for' 112 + ? ts.ForStatement 113 + : M extends 'of' 114 + ? ts.ForOfStatement 115 + : ts.ForInStatement { 116 + this.$validate(); 117 + const body = this.$node(new BlockTsDsl(...this._do).pretty()); 118 + 119 + if (this._mode === 'of') { 120 + return ts.factory.createForOfStatement( 121 + this._await ? ts.factory.createToken(ts.SyntaxKind.AwaitKeyword) : undefined, 122 + this.$node(this._variableOrInit).declarationList, 123 + this.$node(this._iterableOrUpdate), 124 + body, 125 + ) as any; 126 + } 127 + 128 + if (this._mode === 'in') { 129 + return ts.factory.createForInStatement( 130 + this.$node(this._variableOrInit).declarationList, 131 + this.$node(this._iterableOrUpdate), 132 + body, 133 + ) as any; 134 + } 135 + 136 + const init = this.$node(this._variableOrInit); 137 + return ts.factory.createForStatement( 138 + init && ts.isVariableStatement(init) ? init.declarationList : init, 139 + this.$node(this._condition), 140 + this.$node(this._iterableOrUpdate), 141 + body, 142 + ) as any; 143 + } 144 + 145 + $validate(): asserts this is this & { 146 + _iterableOrUpdate: ForIterable; 147 + _variableOrInit: VarTsDsl; 148 + } { 149 + const missing = this.missingRequiredCalls(); 150 + if (missing.length === 0) return; 151 + throw new Error( 152 + `For${this._mode === 'for' ? '' : `...${this._mode}`} statement missing ${missing.join(' and ')}`, 153 + ); 154 + } 155 + 156 + private missingRequiredCalls(): ReadonlyArray<string> { 157 + const missing: Array<string> = []; 158 + if (this._mode !== 'for') { 159 + if (!this._variableOrInit) missing.push('.each()'); 160 + if (!this._iterableOrUpdate) missing.push('.iterable()'); 161 + } 162 + return missing; 163 + } 164 + } 165 + 166 + export const ForTsDsl = ImplForTsDsl as { 167 + new (variableOrInit?: VarTsDsl): ForTsDsl<ForMode>; 168 + new ( 169 + variableOrInit: VarTsDsl, 170 + condition: ForCondition, 171 + iterableOrUpdate?: ForIterable, 172 + ): ForTsDsl<'for'>; 173 + new <T extends ForMode>( 174 + variableOrInit: VarTsDsl, 175 + mode: T, 176 + iterableOrUpdate?: ForIterable, 177 + ): ForTsDsl<T>; 178 + } & typeof ImplForTsDsl; 179 + export type ForTsDsl<M extends ForMode = 'for'> = ImplForTsDsl<M>;
+1 -1
packages/openapi-ts/src/ts-dsl/type/alias.ts
··· 68 68 69 69 private missingRequiredCalls(): ReadonlyArray<string> { 70 70 const missing: Array<string> = []; 71 - if (!this.value) missing.push('.type()'); 71 + if (!this.value) missing.push('.\u200Btype()'); 72 72 return missing; 73 73 } 74 74 }
+1 -1
packages/openapi-ts/src/ts-dsl/type/prop.ts
··· 76 76 77 77 private missingRequiredCalls(): ReadonlyArray<string> { 78 78 const missing: Array<string> = []; 79 - if (!this._type) missing.push('.type()'); 79 + if (!this._type) missing.push('.\u200Btype()'); 80 80 return missing; 81 81 } 82 82 }
+36
specs/3.1.x/transformers-additional-properties.yaml
··· 1 + openapi: 3.1.1 2 + info: 3 + title: OpenAPI 3.1 transformers additional properties example 4 + version: '1' 5 + paths: 6 + /foo: 7 + post: 8 + responses: 9 + '200': 10 + description: OK 11 + content: 12 + application/json: 13 + schema: 14 + $ref: '#/components/schemas/Bar' 15 + components: 16 + schemas: 17 + Foo: 18 + type: object 19 + required: 20 + - foo 21 + properties: 22 + foo: 23 + type: string 24 + format: date-time 25 + Bar: 26 + type: object 27 + required: 28 + - bar 29 + properties: 30 + bar: 31 + type: string 32 + format: date-time 33 + baz: 34 + type: object 35 + additionalProperties: 36 + $ref: '#/components/schemas/Foo'