From f31d3d64e67cae8817b9812508ab7efb015bf379 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 3 May 2026 23:17:06 +0800 Subject: [PATCH 01/14] feat(zod): implement response transformer --- packages/openapi-ts/src/plugins/zod/api.ts | 17 +++++++++++++++++ packages/openapi-ts/src/plugins/zod/mini/api.ts | 7 +++++++ packages/openapi-ts/src/plugins/zod/v3/api.ts | 7 +++++++ packages/openapi-ts/src/plugins/zod/v4/api.ts | 7 +++++++ 4 files changed, 38 insertions(+) diff --git a/packages/openapi-ts/src/plugins/zod/api.ts b/packages/openapi-ts/src/plugins/zod/api.ts index 097ec11769..cdc3e3e680 100644 --- a/packages/openapi-ts/src/plugins/zod/api.ts +++ b/packages/openapi-ts/src/plugins/zod/api.ts @@ -5,6 +5,7 @@ import type { $ } from '../../ts-dsl'; import { createRequestSchemaMini, createRequestValidatorMini, + createResponseTransformerMini, createResponseValidatorMini, } from './mini/api'; import type { Chain } from './shared/chain'; @@ -13,11 +14,13 @@ import type { ZodPlugin } from './types'; import { createRequestSchemaV3, createRequestValidatorV3, + createResponseTransformerV3, createResponseValidatorV3, } from './v3/api'; import { createRequestSchemaV4, createRequestValidatorV4, + createResponseTransformerV4, createResponseValidatorV4, } from './v4/api'; @@ -28,6 +31,7 @@ export type IApi = { createRequestValidator: ( ctx: RequestSchemaContext, ) => ReturnType | undefined; + createResponseTransformer: (ctx: ValidatorArgs) => ReturnType | undefined; createResponseValidator: (ctx: ValidatorArgs) => ReturnType | undefined; }; @@ -64,6 +68,19 @@ export class Api implements IApi { } } + createResponseTransformer(ctx: ValidatorArgs): ReturnType | undefined { + const { plugin } = ctx; + switch (plugin.config.compatibilityVersion) { + case 3: + return createResponseTransformerV3(ctx); + case 'mini': + return createResponseTransformerMini(ctx); + case 4: + default: + return createResponseTransformerV4(ctx); + } + } + createResponseValidator(ctx: ValidatorArgs): ReturnType | undefined { const { plugin } = ctx; switch (plugin.config.compatibilityVersion) { diff --git a/packages/openapi-ts/src/plugins/zod/mini/api.ts b/packages/openapi-ts/src/plugins/zod/mini/api.ts index 9ff5e7e37f..5b1741cb79 100644 --- a/packages/openapi-ts/src/plugins/zod/mini/api.ts +++ b/packages/openapi-ts/src/plugins/zod/mini/api.ts @@ -227,3 +227,10 @@ export function createResponseValidatorMini({ }; return runResponseResolver(resolverCtx); } + +export function createResponseTransformerMini({ + operation, + plugin, +}: ValidatorArgs): ReturnType | undefined { + return createResponseValidatorMini({ operation, plugin }); +} diff --git a/packages/openapi-ts/src/plugins/zod/v3/api.ts b/packages/openapi-ts/src/plugins/zod/v3/api.ts index 7200e48164..9d4b0459b0 100644 --- a/packages/openapi-ts/src/plugins/zod/v3/api.ts +++ b/packages/openapi-ts/src/plugins/zod/v3/api.ts @@ -226,3 +226,10 @@ export function createResponseValidatorV3({ }; return runResponseResolver(resolverCtx); } + +export function createResponseTransformerV3({ + operation, + plugin, +}: ValidatorArgs): ReturnType | undefined { + return createResponseValidatorV3({ operation, plugin }); +} diff --git a/packages/openapi-ts/src/plugins/zod/v4/api.ts b/packages/openapi-ts/src/plugins/zod/v4/api.ts index 7e3c84149e..bcd613891a 100644 --- a/packages/openapi-ts/src/plugins/zod/v4/api.ts +++ b/packages/openapi-ts/src/plugins/zod/v4/api.ts @@ -226,3 +226,10 @@ export function createResponseValidatorV4({ }; return runResponseResolver(resolverCtx); } + +export function createResponseTransformerV4({ + operation, + plugin, +}: ValidatorArgs): ReturnType | undefined { + return createResponseValidatorV4({ operation, plugin }); +} From 665fb3ba4ffdf8ef83de18ecdd6f03b96eae34df Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 3 May 2026 23:20:20 +0800 Subject: [PATCH 02/14] feat(sdk): add `zod` to new transformer type --- .../src/plugins/@hey-api/sdk/config.ts | 35 ++++++++++----- .../src/plugins/@hey-api/sdk/types.ts | 44 ++++++++++++++----- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts index 34634c572c..95e84dac09 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts @@ -7,7 +7,7 @@ import { handler } from './plugin'; import type { HeyApiSdkPlugin } from './types'; const transformerInferWarn = - 'You set `transformer: true` but no transformer plugin was found in your plugins. Add a transformer plugin like `@hey-api/transformers` to enable this feature. The transformer option has been disabled.'; + 'You set `transformer: true` but no transformer plugin was found in your plugins. Add a transformer plugin like `@hey-api/transformers` or a validator plugin like `zod` to enable this feature. The transformer option has been disabled.'; const validatorInferWarn = 'You set `validator: true` but no validator plugin was found in your plugins. Add a validator plugin like `zod` to enable this feature. The validator option has been disabled.'; @@ -19,7 +19,7 @@ export const defaultConfig: HeyApiSdkPlugin['Config'] = { includeInEntry: true, paramsStructure: 'grouped', responseStyle: 'fields', - transformer: false, + transformer: { response: false }, validator: false, // Deprecated - kept for backward compatibility @@ -42,20 +42,35 @@ export const defaultConfig: HeyApiSdkPlugin['Config'] = { plugin.config.client = false; } - if (plugin.config.transformer) { - if (typeof plugin.config.transformer === 'boolean') { + // Normalize transformer to object form + if (typeof plugin.config.transformer !== 'object' || plugin.config.transformer === null) { + plugin.config.transformer = { + response: plugin.config.transformer as Exclude, + }; + } + + if (plugin.config.transformer.response) { + if (typeof plugin.config.transformer.response === 'boolean') { + // `true`: auto-select transformer plugin, fall back to validator plugin + let resolved: string | undefined | false = false; try { - plugin.config.transformer = context.pluginByTag('transformer'); - plugin.dependencies.add(plugin.config.transformer!); + resolved = context.pluginByTag('transformer'); } catch { - log.warn(transformerInferWarn); - plugin.config.transformer = false; + try { + resolved = context.pluginByTag('validator'); + } catch { + log.warn(transformerInferWarn); + } + } + plugin.config.transformer.response = resolved as typeof plugin.config.transformer.response; + if (resolved) { + plugin.dependencies.add(resolved); } } else { - plugin.dependencies.add(plugin.config.transformer); + plugin.dependencies.add(plugin.config.transformer.response); } } else { - plugin.config.transformer = false; + plugin.config.transformer.response = false; } if (typeof plugin.config.validator !== 'object') { diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/types.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.ts index ec5497d6c7..b8da559cb2 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/types.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.ts @@ -80,9 +80,30 @@ export type UserConfig = Plugin.Name<'@hey-api/sdk'> & * can also set `transformer` to `true` to automatically choose the * transformer from your defined plugins. * + * When set to `'zod'`, the Zod plugin's `parseAsync` is used as the + * response transformer, which applies Zod's coercion and transformation + * rules (e.g., converting strings to `Date` objects via `z.coerce.date()`). + * This requires the `zod` plugin to be configured in your plugins. + * + * Use the object form for fine-grained control over response transformation. + * * @default false */ - transformer?: PluginTransformerNames | boolean; + transformer?: + | PluginTransformerNames + | 'zod' + | boolean + | { + /** + * Transform response data using the specified transformer. + * + * Can be a transformer plugin name, `'zod'`, or a boolean (`true` to + * auto-select, `false` to disable). + * + * @default false + */ + response?: PluginTransformerNames | 'zod' | boolean; + }; /** * Validate request and/or response data against schema before returning. * This is useful if you want to ensure the request and/or response conforms @@ -191,6 +212,7 @@ export type UserConfig = Plugin.Name<'@hey-api/sdk'> & response?: 'body' | 'response'; }; +/** Normalized configuration */ export type Config = Plugin.Name<'@hey-api/sdk'> & Plugin.Hooks & Plugin.Comments & @@ -239,17 +261,17 @@ export type Config = Plugin.Name<'@hey-api/sdk'> & */ responseStyle: 'data' | 'fields'; /** - * Transform response data before returning. This is useful if you want to - * convert for example ISO strings into Date objects. However, transformation - * adds runtime overhead, so it's not recommended to use unless necessary. - * - * You can customize the selected transformer output through its plugin. You - * can also set `transformer` to `true` to automatically choose the - * transformer from your defined plugins. - * - * @default false + * Configuration for response data transformation. */ - transformer: PluginTransformerNames | false; + transformer: { + /** + * The transformer plugin to use for response transformation, or false to + * disable. + * + * @default false + */ + response: PluginTransformerNames | 'zod' | false; + }; /** * Validate request and/or response data against schema before returning. * This is useful if you want to ensure the request and/or response conforms From 03db4a48e3058bad822dc43724c54555a2abb980 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 3 May 2026 23:21:24 +0800 Subject: [PATCH 03/14] feat(sdk): adapt response transformer to new format --- .../plugins/@hey-api/sdk/shared/operation.ts | 31 +++++++++++++------ .../@hey-api/sdk/shared/transformer.ts | 30 ++++++++++++++++++ 2 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts index 3e54a36257..9ad6bc49d7 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts @@ -12,6 +12,7 @@ import { isInstance } from '../v1/node'; import { operationAuth } from './auth'; import { nuxtTypeComposable, nuxtTypeDefault } from './constants'; import { getSignatureParameters } from './signature'; +import { createResponseTransformer } from './transformer'; import { createRequestValidator, createResponseValidator } from './validator'; /** TODO: needs complete refactor */ @@ -339,16 +340,26 @@ export function operationStatements({ reqOptions.prop('requestValidator', requestValidator.arrow()); } - if (plugin.config.transformer) { - const query: SymbolMeta = { - category: 'transform', - resource: 'operation', - resourceId: operation.id, - role: 'response', - }; - if (plugin.isSymbolRegistered(query)) { - const ref = plugin.referenceSymbol(query); - reqOptions.prop('responseTransformer', $(ref)); + if (plugin.config.transformer.response) { + const transformerPlugin = plugin.getPlugin(plugin.config.transformer.response); + if (transformerPlugin?.api && 'createResponseTransformer' in transformerPlugin.api) { + // Zod-style transformer: inline arrow function wrapping parseAsync + const responseTransformerFn = createResponseTransformer({ operation, plugin }); + if (responseTransformerFn) { + reqOptions.prop('responseTransformer', responseTransformerFn.arrow()); + } + } else { + // @hey-api/transformers-style: reference to a named transformer function + const query: SymbolMeta = { + category: 'transform', + resource: 'operation', + resourceId: operation.id, + role: 'response', + }; + if (plugin.isSymbolRegistered(query)) { + const ref = plugin.referenceSymbol(query); + reqOptions.prop('responseTransformer', $(ref)); + } } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts new file mode 100644 index 0000000000..062040cee8 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts @@ -0,0 +1,30 @@ +import type { IR } from '@hey-api/shared'; + +import type { $ } from '../../../../ts-dsl'; +import type { HeyApiSdkPlugin } from '../types'; + +export function createResponseTransformer({ + operation, + plugin, +}: { + /** The operation object. */ + operation: IR.OperationObject; + /** The plugin instance. */ + plugin: HeyApiSdkPlugin['Instance']; +}): ReturnType | undefined { + const { response } = plugin.config.transformer; + if (!response) return; + + const transformer = plugin.getPluginOrThrow(response); + if ( + !transformer.api || + !('createResponseTransformer' in transformer.api) || + typeof transformer.api.createResponseTransformer !== 'function' + ) + return; + + return transformer.api.createResponseTransformer({ + operation, + plugin: transformer, + }); +} From b2a3b8f57d8ab66a144d5bc36a5e67334a795612 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 3 May 2026 23:39:16 +0800 Subject: [PATCH 04/14] test(zod): add test cases for zod transformer support --- .../2.0.x/mini/zod-transformer/client.gen.ts | 16 + .../mini/zod-transformer/client/client.gen.ts | 280 +++++++++++++++ .../mini/zod-transformer/client/index.ts | 25 ++ .../mini/zod-transformer/client/types.gen.ts | 217 ++++++++++++ .../mini/zod-transformer/client/utils.gen.ts | 318 ++++++++++++++++++ .../mini/zod-transformer/core/auth.gen.ts | 41 +++ .../core/bodySerializer.gen.ts | 82 +++++ .../mini/zod-transformer/core/params.gen.ts | 169 ++++++++++ .../core/pathSerializer.gen.ts | 171 ++++++++++ .../core/queryKeySerializer.gen.ts | 117 +++++++ .../core/serverSentEvents.gen.ts | 242 +++++++++++++ .../mini/zod-transformer/core/types.gen.ts | 104 ++++++ .../mini/zod-transformer/core/utils.gen.ts | 140 ++++++++ .../2.0.x/mini/zod-transformer/index.ts | 4 + .../2.0.x/mini/zod-transformer/sdk.gen.ts | 34 ++ .../2.0.x/mini/zod-transformer/types.gen.ts | 36 ++ .../2.0.x/mini/zod-transformer/zod.gen.ts | 18 + .../2.0.x/v3/zod-transformer/client.gen.ts | 16 + .../v3/zod-transformer/client/client.gen.ts | 280 +++++++++++++++ .../2.0.x/v3/zod-transformer/client/index.ts | 25 ++ .../v3/zod-transformer/client/types.gen.ts | 217 ++++++++++++ .../v3/zod-transformer/client/utils.gen.ts | 318 ++++++++++++++++++ .../2.0.x/v3/zod-transformer/core/auth.gen.ts | 41 +++ .../core/bodySerializer.gen.ts | 82 +++++ .../v3/zod-transformer/core/params.gen.ts | 169 ++++++++++ .../core/pathSerializer.gen.ts | 171 ++++++++++ .../core/queryKeySerializer.gen.ts | 117 +++++++ .../core/serverSentEvents.gen.ts | 242 +++++++++++++ .../v3/zod-transformer/core/types.gen.ts | 104 ++++++ .../v3/zod-transformer/core/utils.gen.ts | 140 ++++++++ .../2.0.x/v3/zod-transformer/index.ts | 4 + .../2.0.x/v3/zod-transformer/sdk.gen.ts | 34 ++ .../2.0.x/v3/zod-transformer/types.gen.ts | 36 ++ .../2.0.x/v3/zod-transformer/zod.gen.ts | 18 + .../2.0.x/v4/zod-transformer/client.gen.ts | 16 + .../v4/zod-transformer/client/client.gen.ts | 280 +++++++++++++++ .../2.0.x/v4/zod-transformer/client/index.ts | 25 ++ .../v4/zod-transformer/client/types.gen.ts | 217 ++++++++++++ .../v4/zod-transformer/client/utils.gen.ts | 318 ++++++++++++++++++ .../2.0.x/v4/zod-transformer/core/auth.gen.ts | 41 +++ .../core/bodySerializer.gen.ts | 82 +++++ .../v4/zod-transformer/core/params.gen.ts | 169 ++++++++++ .../core/pathSerializer.gen.ts | 171 ++++++++++ .../core/queryKeySerializer.gen.ts | 117 +++++++ .../core/serverSentEvents.gen.ts | 242 +++++++++++++ .../v4/zod-transformer/core/types.gen.ts | 104 ++++++ .../v4/zod-transformer/core/utils.gen.ts | 140 ++++++++ .../2.0.x/v4/zod-transformer/index.ts | 4 + .../2.0.x/v4/zod-transformer/sdk.gen.ts | 34 ++ .../2.0.x/v4/zod-transformer/types.gen.ts | 36 ++ .../2.0.x/v4/zod-transformer/zod.gen.ts | 18 + .../3.0.x/mini/zod-transformer/client.gen.ts | 16 + .../mini/zod-transformer/client/client.gen.ts | 280 +++++++++++++++ .../mini/zod-transformer/client/index.ts | 25 ++ .../mini/zod-transformer/client/types.gen.ts | 217 ++++++++++++ .../mini/zod-transformer/client/utils.gen.ts | 318 ++++++++++++++++++ .../mini/zod-transformer/core/auth.gen.ts | 41 +++ .../core/bodySerializer.gen.ts | 82 +++++ .../mini/zod-transformer/core/params.gen.ts | 169 ++++++++++ .../core/pathSerializer.gen.ts | 171 ++++++++++ .../core/queryKeySerializer.gen.ts | 117 +++++++ .../core/serverSentEvents.gen.ts | 242 +++++++++++++ .../mini/zod-transformer/core/types.gen.ts | 104 ++++++ .../mini/zod-transformer/core/utils.gen.ts | 140 ++++++++ .../3.0.x/mini/zod-transformer/index.ts | 4 + .../3.0.x/mini/zod-transformer/sdk.gen.ts | 34 ++ .../3.0.x/mini/zod-transformer/types.gen.ts | 36 ++ .../3.0.x/mini/zod-transformer/zod.gen.ts | 18 + .../3.0.x/v3/zod-transformer/client.gen.ts | 16 + .../v3/zod-transformer/client/client.gen.ts | 280 +++++++++++++++ .../3.0.x/v3/zod-transformer/client/index.ts | 25 ++ .../v3/zod-transformer/client/types.gen.ts | 217 ++++++++++++ .../v3/zod-transformer/client/utils.gen.ts | 318 ++++++++++++++++++ .../3.0.x/v3/zod-transformer/core/auth.gen.ts | 41 +++ .../core/bodySerializer.gen.ts | 82 +++++ .../v3/zod-transformer/core/params.gen.ts | 169 ++++++++++ .../core/pathSerializer.gen.ts | 171 ++++++++++ .../core/queryKeySerializer.gen.ts | 117 +++++++ .../core/serverSentEvents.gen.ts | 242 +++++++++++++ .../v3/zod-transformer/core/types.gen.ts | 104 ++++++ .../v3/zod-transformer/core/utils.gen.ts | 140 ++++++++ .../3.0.x/v3/zod-transformer/index.ts | 4 + .../3.0.x/v3/zod-transformer/sdk.gen.ts | 34 ++ .../3.0.x/v3/zod-transformer/types.gen.ts | 36 ++ .../3.0.x/v3/zod-transformer/zod.gen.ts | 18 + .../3.0.x/v4/zod-transformer/client.gen.ts | 16 + .../v4/zod-transformer/client/client.gen.ts | 280 +++++++++++++++ .../3.0.x/v4/zod-transformer/client/index.ts | 25 ++ .../v4/zod-transformer/client/types.gen.ts | 217 ++++++++++++ .../v4/zod-transformer/client/utils.gen.ts | 318 ++++++++++++++++++ .../3.0.x/v4/zod-transformer/core/auth.gen.ts | 41 +++ .../core/bodySerializer.gen.ts | 82 +++++ .../v4/zod-transformer/core/params.gen.ts | 169 ++++++++++ .../core/pathSerializer.gen.ts | 171 ++++++++++ .../core/queryKeySerializer.gen.ts | 117 +++++++ .../core/serverSentEvents.gen.ts | 242 +++++++++++++ .../v4/zod-transformer/core/types.gen.ts | 104 ++++++ .../v4/zod-transformer/core/utils.gen.ts | 140 ++++++++ .../3.0.x/v4/zod-transformer/index.ts | 4 + .../3.0.x/v4/zod-transformer/sdk.gen.ts | 34 ++ .../3.0.x/v4/zod-transformer/types.gen.ts | 36 ++ .../3.0.x/v4/zod-transformer/zod.gen.ts | 18 + .../3.1.x/mini/zod-transformer/client.gen.ts | 16 + .../mini/zod-transformer/client/client.gen.ts | 280 +++++++++++++++ .../mini/zod-transformer/client/index.ts | 25 ++ .../mini/zod-transformer/client/types.gen.ts | 217 ++++++++++++ .../mini/zod-transformer/client/utils.gen.ts | 318 ++++++++++++++++++ .../mini/zod-transformer/core/auth.gen.ts | 41 +++ .../core/bodySerializer.gen.ts | 82 +++++ .../mini/zod-transformer/core/params.gen.ts | 169 ++++++++++ .../core/pathSerializer.gen.ts | 171 ++++++++++ .../core/queryKeySerializer.gen.ts | 117 +++++++ .../core/serverSentEvents.gen.ts | 242 +++++++++++++ .../mini/zod-transformer/core/types.gen.ts | 104 ++++++ .../mini/zod-transformer/core/utils.gen.ts | 140 ++++++++ .../3.1.x/mini/zod-transformer/index.ts | 4 + .../3.1.x/mini/zod-transformer/sdk.gen.ts | 34 ++ .../3.1.x/mini/zod-transformer/types.gen.ts | 36 ++ .../3.1.x/mini/zod-transformer/zod.gen.ts | 18 + .../3.1.x/v3/zod-transformer/client.gen.ts | 16 + .../v3/zod-transformer/client/client.gen.ts | 280 +++++++++++++++ .../3.1.x/v3/zod-transformer/client/index.ts | 25 ++ .../v3/zod-transformer/client/types.gen.ts | 217 ++++++++++++ .../v3/zod-transformer/client/utils.gen.ts | 318 ++++++++++++++++++ .../3.1.x/v3/zod-transformer/core/auth.gen.ts | 41 +++ .../core/bodySerializer.gen.ts | 82 +++++ .../v3/zod-transformer/core/params.gen.ts | 169 ++++++++++ .../core/pathSerializer.gen.ts | 171 ++++++++++ .../core/queryKeySerializer.gen.ts | 117 +++++++ .../core/serverSentEvents.gen.ts | 242 +++++++++++++ .../v3/zod-transformer/core/types.gen.ts | 104 ++++++ .../v3/zod-transformer/core/utils.gen.ts | 140 ++++++++ .../3.1.x/v3/zod-transformer/index.ts | 4 + .../3.1.x/v3/zod-transformer/sdk.gen.ts | 34 ++ .../3.1.x/v3/zod-transformer/types.gen.ts | 36 ++ .../3.1.x/v3/zod-transformer/zod.gen.ts | 18 + .../3.1.x/v4/zod-transformer/client.gen.ts | 16 + .../v4/zod-transformer/client/client.gen.ts | 280 +++++++++++++++ .../3.1.x/v4/zod-transformer/client/index.ts | 25 ++ .../v4/zod-transformer/client/types.gen.ts | 217 ++++++++++++ .../v4/zod-transformer/client/utils.gen.ts | 318 ++++++++++++++++++ .../3.1.x/v4/zod-transformer/core/auth.gen.ts | 41 +++ .../core/bodySerializer.gen.ts | 82 +++++ .../v4/zod-transformer/core/params.gen.ts | 169 ++++++++++ .../core/pathSerializer.gen.ts | 171 ++++++++++ .../core/queryKeySerializer.gen.ts | 117 +++++++ .../core/serverSentEvents.gen.ts | 242 +++++++++++++ .../v4/zod-transformer/core/types.gen.ts | 104 ++++++ .../v4/zod-transformer/core/utils.gen.ts | 140 ++++++++ .../3.1.x/v4/zod-transformer/index.ts | 4 + .../3.1.x/v4/zod-transformer/sdk.gen.ts | 34 ++ .../3.1.x/v4/zod-transformer/types.gen.ts | 36 ++ .../3.1.x/v4/zod-transformer/zod.gen.ts | 18 + .../zod/v4/test/openapi.test.ts | 19 ++ 154 files changed, 18145 insertions(+) create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/auth.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/bodySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/params.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/pathSerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/sdk.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/auth.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/bodySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/params.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/pathSerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/sdk.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/auth.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/bodySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/params.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/pathSerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/sdk.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/auth.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/bodySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/params.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/pathSerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/sdk.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/auth.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/bodySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/params.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/pathSerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/sdk.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/auth.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/bodySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/params.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/pathSerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/sdk.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/auth.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/bodySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/params.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/pathSerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/queryKeySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/serverSentEvents.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/sdk.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/auth.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/bodySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/params.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/pathSerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/queryKeySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/serverSentEvents.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/sdk.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/auth.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/bodySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/params.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/pathSerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/queryKeySerializer.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/serverSentEvents.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/utils.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/index.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/sdk.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/types.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/zod.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client.gen.ts new file mode 100644 index 0000000000..cab3c70195 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/client.gen.ts new file mode 100644 index 0000000000..377b6c9e09 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/client.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & + ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async (options) => { + const throwOnError = options.throwOnError ?? _config.throwOnError; + const responseStyle = options.responseStyle ?? _config.responseStyle; + + let request: Request | undefined; + let response: Response | undefined; + + try { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + + response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + throw jsonError ?? textError; + } catch (error) { + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); + } + } + + finalError = finalError || {}; + + if (throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response, + }; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/index.ts new file mode 100644 index 0000000000..b295edeca0 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/types.gen.ts new file mode 100644 index 0000000000..4b288a5099 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/types.gen.ts @@ -0,0 +1,217 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + /** request may be undefined, because error may be from building the request object itself */ + request?: Request; + /** response may be undefined, because error may be from building the request object itself or from a network error */ + response?: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/utils.gen.ts new file mode 100644 index 0000000000..eb0164fa8c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/utils.gen.ts @@ -0,0 +1,318 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + /** response may be undefined due to a network error where no response object is produced */ + response: Res | undefined, + /** request may be undefined, because error may be from building the request object itself */ + request: Req | undefined, + options: Options, +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/auth.gen.ts new file mode 100644 index 0000000000..3ebf994788 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/bodySerializer.gen.ts new file mode 100644 index 0000000000..67daca60f8 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/params.gen.ts new file mode 100644 index 0000000000..7955601a5c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/pathSerializer.gen.ts new file mode 100644 index 0000000000..994b2848c6 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000000..5000df606f --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts new file mode 100644 index 0000000000..ddf3c4d13a --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/types.gen.ts new file mode 100644 index 0000000000..9efe71d4c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/utils.gen.ts new file mode 100644 index 0000000000..9a4fec7830 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { type Options, postFoo } from './sdk.gen'; +export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/sdk.gen.ts new file mode 100644 index 0000000000..f741139140 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/sdk.gen.ts @@ -0,0 +1,34 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { PostFooData, PostFooResponses } from './types.gen'; +import { zPostFooResponse } from './zod.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +export const postFoo = (options?: Options) => (options?.client ?? client).post({ + requestValidator: async (data) => await z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.never()) + }).parseAsync(data), + responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), + responseValidator: async (data) => await zPostFooResponse.parseAsync(data), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/types.gen.ts new file mode 100644 index 0000000000..9c2a6c4718 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/types.gen.ts @@ -0,0 +1,36 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: string; +}; + +export type TypeID = `${T}_${string}`; + +export type UserId = TypeID<'user'>; + +export type Foo = { + bar?: number; + foo: number; + id: UserId; +}; + +export type Bar = { + foo: number; + [key: string]: number; +}; + +export type PostFooData = { + body?: never; + path?: never; + query?: never; + url: '/foo'; +}; + +export type PostFooResponses = { + /** + * OK + */ + 200: Foo; +}; + +export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/zod.gen.ts new file mode 100644 index 0000000000..4820089703 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/zod.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +export const zFoo = z.object({ + bar: z.optional(z.int()), + foo: z._default(z.coerce.bigint().check(z.minimum(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }), z.maximum(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' })), BigInt(0)), + id: z.string() +}); + +export const zBar = z.object({ + foo: z.int() +}); + +/** + * OK + */ +export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client.gen.ts new file mode 100644 index 0000000000..cab3c70195 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/client.gen.ts new file mode 100644 index 0000000000..377b6c9e09 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/client.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & + ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async (options) => { + const throwOnError = options.throwOnError ?? _config.throwOnError; + const responseStyle = options.responseStyle ?? _config.responseStyle; + + let request: Request | undefined; + let response: Response | undefined; + + try { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + + response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + throw jsonError ?? textError; + } catch (error) { + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); + } + } + + finalError = finalError || {}; + + if (throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response, + }; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/index.ts new file mode 100644 index 0000000000..b295edeca0 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/types.gen.ts new file mode 100644 index 0000000000..4b288a5099 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/types.gen.ts @@ -0,0 +1,217 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + /** request may be undefined, because error may be from building the request object itself */ + request?: Request; + /** response may be undefined, because error may be from building the request object itself or from a network error */ + response?: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/utils.gen.ts new file mode 100644 index 0000000000..eb0164fa8c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/utils.gen.ts @@ -0,0 +1,318 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + /** response may be undefined due to a network error where no response object is produced */ + response: Res | undefined, + /** request may be undefined, because error may be from building the request object itself */ + request: Req | undefined, + options: Options, +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/auth.gen.ts new file mode 100644 index 0000000000..3ebf994788 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/bodySerializer.gen.ts new file mode 100644 index 0000000000..67daca60f8 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/params.gen.ts new file mode 100644 index 0000000000..7955601a5c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/pathSerializer.gen.ts new file mode 100644 index 0000000000..994b2848c6 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000000..5000df606f --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts new file mode 100644 index 0000000000..ddf3c4d13a --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/types.gen.ts new file mode 100644 index 0000000000..9efe71d4c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/utils.gen.ts new file mode 100644 index 0000000000..9a4fec7830 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { type Options, postFoo } from './sdk.gen'; +export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/sdk.gen.ts new file mode 100644 index 0000000000..f95fb60307 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/sdk.gen.ts @@ -0,0 +1,34 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { PostFooData, PostFooResponses } from './types.gen'; +import { zPostFooResponse } from './zod.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +export const postFoo = (options?: Options) => (options?.client ?? client).post({ + requestValidator: async (data) => await z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.never().optional() + }).parseAsync(data), + responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), + responseValidator: async (data) => await zPostFooResponse.parseAsync(data), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/types.gen.ts new file mode 100644 index 0000000000..9c2a6c4718 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/types.gen.ts @@ -0,0 +1,36 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: string; +}; + +export type TypeID = `${T}_${string}`; + +export type UserId = TypeID<'user'>; + +export type Foo = { + bar?: number; + foo: number; + id: UserId; +}; + +export type Bar = { + foo: number; + [key: string]: number; +}; + +export type PostFooData = { + body?: never; + path?: never; + query?: never; + url: '/foo'; +}; + +export type PostFooResponses = { + /** + * OK + */ + 200: Foo; +}; + +export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/zod.gen.ts new file mode 100644 index 0000000000..01c7ccbc5b --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/zod.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +export const zFoo = z.object({ + bar: z.number().int().optional(), + foo: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).default(BigInt(0)), + id: z.string() +}); + +export const zBar = z.object({ + foo: z.number().int() +}); + +/** + * OK + */ +export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client.gen.ts new file mode 100644 index 0000000000..cab3c70195 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/client.gen.ts new file mode 100644 index 0000000000..377b6c9e09 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/client.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & + ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async (options) => { + const throwOnError = options.throwOnError ?? _config.throwOnError; + const responseStyle = options.responseStyle ?? _config.responseStyle; + + let request: Request | undefined; + let response: Response | undefined; + + try { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + + response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + throw jsonError ?? textError; + } catch (error) { + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); + } + } + + finalError = finalError || {}; + + if (throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response, + }; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/index.ts new file mode 100644 index 0000000000..b295edeca0 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/types.gen.ts new file mode 100644 index 0000000000..4b288a5099 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/types.gen.ts @@ -0,0 +1,217 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + /** request may be undefined, because error may be from building the request object itself */ + request?: Request; + /** response may be undefined, because error may be from building the request object itself or from a network error */ + response?: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/utils.gen.ts new file mode 100644 index 0000000000..eb0164fa8c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/utils.gen.ts @@ -0,0 +1,318 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + /** response may be undefined due to a network error where no response object is produced */ + response: Res | undefined, + /** request may be undefined, because error may be from building the request object itself */ + request: Req | undefined, + options: Options, +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/auth.gen.ts new file mode 100644 index 0000000000..3ebf994788 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/bodySerializer.gen.ts new file mode 100644 index 0000000000..67daca60f8 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/params.gen.ts new file mode 100644 index 0000000000..7955601a5c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/pathSerializer.gen.ts new file mode 100644 index 0000000000..994b2848c6 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000000..5000df606f --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts new file mode 100644 index 0000000000..ddf3c4d13a --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/types.gen.ts new file mode 100644 index 0000000000..9efe71d4c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/utils.gen.ts new file mode 100644 index 0000000000..9a4fec7830 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { type Options, postFoo } from './sdk.gen'; +export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/sdk.gen.ts new file mode 100644 index 0000000000..2a0fe9d412 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/sdk.gen.ts @@ -0,0 +1,34 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod'; + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { PostFooData, PostFooResponses } from './types.gen'; +import { zPostFooResponse } from './zod.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +export const postFoo = (options?: Options) => (options?.client ?? client).post({ + requestValidator: async (data) => await z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.never().optional() + }).parseAsync(data), + responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), + responseValidator: async (data) => await zPostFooResponse.parseAsync(data), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/types.gen.ts new file mode 100644 index 0000000000..9c2a6c4718 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/types.gen.ts @@ -0,0 +1,36 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: string; +}; + +export type TypeID = `${T}_${string}`; + +export type UserId = TypeID<'user'>; + +export type Foo = { + bar?: number; + foo: number; + id: UserId; +}; + +export type Bar = { + foo: number; + [key: string]: number; +}; + +export type PostFooData = { + body?: never; + path?: never; + query?: never; + url: '/foo'; +}; + +export type PostFooResponses = { + /** + * OK + */ + 200: Foo; +}; + +export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/zod.gen.ts new file mode 100644 index 0000000000..6173cb8206 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/zod.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod'; + +export const zFoo = z.object({ + bar: z.int().optional(), + foo: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).default(BigInt(0)), + id: z.string() +}); + +export const zBar = z.object({ + foo: z.int() +}); + +/** + * OK + */ +export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client.gen.ts new file mode 100644 index 0000000000..cab3c70195 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts new file mode 100644 index 0000000000..377b6c9e09 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & + ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async (options) => { + const throwOnError = options.throwOnError ?? _config.throwOnError; + const responseStyle = options.responseStyle ?? _config.responseStyle; + + let request: Request | undefined; + let response: Response | undefined; + + try { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + + response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + throw jsonError ?? textError; + } catch (error) { + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); + } + } + + finalError = finalError || {}; + + if (throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response, + }; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/index.ts new file mode 100644 index 0000000000..b295edeca0 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/types.gen.ts new file mode 100644 index 0000000000..4b288a5099 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/types.gen.ts @@ -0,0 +1,217 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + /** request may be undefined, because error may be from building the request object itself */ + request?: Request; + /** response may be undefined, because error may be from building the request object itself or from a network error */ + response?: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts new file mode 100644 index 0000000000..eb0164fa8c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts @@ -0,0 +1,318 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + /** response may be undefined due to a network error where no response object is produced */ + response: Res | undefined, + /** request may be undefined, because error may be from building the request object itself */ + request: Req | undefined, + options: Options, +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/auth.gen.ts new file mode 100644 index 0000000000..3ebf994788 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/bodySerializer.gen.ts new file mode 100644 index 0000000000..67daca60f8 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/params.gen.ts new file mode 100644 index 0000000000..7955601a5c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/pathSerializer.gen.ts new file mode 100644 index 0000000000..994b2848c6 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000000..5000df606f --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts new file mode 100644 index 0000000000..ddf3c4d13a --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/types.gen.ts new file mode 100644 index 0000000000..9efe71d4c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/utils.gen.ts new file mode 100644 index 0000000000..9a4fec7830 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { type Options, postFoo } from './sdk.gen'; +export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/sdk.gen.ts new file mode 100644 index 0000000000..f741139140 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/sdk.gen.ts @@ -0,0 +1,34 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { PostFooData, PostFooResponses } from './types.gen'; +import { zPostFooResponse } from './zod.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +export const postFoo = (options?: Options) => (options?.client ?? client).post({ + requestValidator: async (data) => await z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.never()) + }).parseAsync(data), + responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), + responseValidator: async (data) => await zPostFooResponse.parseAsync(data), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/types.gen.ts new file mode 100644 index 0000000000..318823eb66 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/types.gen.ts @@ -0,0 +1,36 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type TypeID = `${T}_${string}`; + +export type UserId = TypeID<'user'>; + +export type Foo = { + bar?: number; + foo: number; + id: UserId; +}; + +export type Bar = { + foo: number; + [key: string]: number; +}; + +export type PostFooData = { + body?: never; + path?: never; + query?: never; + url: '/foo'; +}; + +export type PostFooResponses = { + /** + * OK + */ + 200: Foo; +}; + +export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/zod.gen.ts new file mode 100644 index 0000000000..4820089703 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/zod.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +export const zFoo = z.object({ + bar: z.optional(z.int()), + foo: z._default(z.coerce.bigint().check(z.minimum(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }), z.maximum(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' })), BigInt(0)), + id: z.string() +}); + +export const zBar = z.object({ + foo: z.int() +}); + +/** + * OK + */ +export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client.gen.ts new file mode 100644 index 0000000000..cab3c70195 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts new file mode 100644 index 0000000000..377b6c9e09 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & + ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async (options) => { + const throwOnError = options.throwOnError ?? _config.throwOnError; + const responseStyle = options.responseStyle ?? _config.responseStyle; + + let request: Request | undefined; + let response: Response | undefined; + + try { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + + response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + throw jsonError ?? textError; + } catch (error) { + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); + } + } + + finalError = finalError || {}; + + if (throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response, + }; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/index.ts new file mode 100644 index 0000000000..b295edeca0 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/types.gen.ts new file mode 100644 index 0000000000..4b288a5099 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/types.gen.ts @@ -0,0 +1,217 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + /** request may be undefined, because error may be from building the request object itself */ + request?: Request; + /** response may be undefined, because error may be from building the request object itself or from a network error */ + response?: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts new file mode 100644 index 0000000000..eb0164fa8c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts @@ -0,0 +1,318 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + /** response may be undefined due to a network error where no response object is produced */ + response: Res | undefined, + /** request may be undefined, because error may be from building the request object itself */ + request: Req | undefined, + options: Options, +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/auth.gen.ts new file mode 100644 index 0000000000..3ebf994788 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/bodySerializer.gen.ts new file mode 100644 index 0000000000..67daca60f8 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/params.gen.ts new file mode 100644 index 0000000000..7955601a5c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/pathSerializer.gen.ts new file mode 100644 index 0000000000..994b2848c6 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000000..5000df606f --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts new file mode 100644 index 0000000000..ddf3c4d13a --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/types.gen.ts new file mode 100644 index 0000000000..9efe71d4c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/utils.gen.ts new file mode 100644 index 0000000000..9a4fec7830 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { type Options, postFoo } from './sdk.gen'; +export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/sdk.gen.ts new file mode 100644 index 0000000000..f95fb60307 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/sdk.gen.ts @@ -0,0 +1,34 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { PostFooData, PostFooResponses } from './types.gen'; +import { zPostFooResponse } from './zod.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +export const postFoo = (options?: Options) => (options?.client ?? client).post({ + requestValidator: async (data) => await z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.never().optional() + }).parseAsync(data), + responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), + responseValidator: async (data) => await zPostFooResponse.parseAsync(data), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/types.gen.ts new file mode 100644 index 0000000000..318823eb66 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/types.gen.ts @@ -0,0 +1,36 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type TypeID = `${T}_${string}`; + +export type UserId = TypeID<'user'>; + +export type Foo = { + bar?: number; + foo: number; + id: UserId; +}; + +export type Bar = { + foo: number; + [key: string]: number; +}; + +export type PostFooData = { + body?: never; + path?: never; + query?: never; + url: '/foo'; +}; + +export type PostFooResponses = { + /** + * OK + */ + 200: Foo; +}; + +export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/zod.gen.ts new file mode 100644 index 0000000000..01c7ccbc5b --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/zod.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +export const zFoo = z.object({ + bar: z.number().int().optional(), + foo: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).default(BigInt(0)), + id: z.string() +}); + +export const zBar = z.object({ + foo: z.number().int() +}); + +/** + * OK + */ +export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client.gen.ts new file mode 100644 index 0000000000..cab3c70195 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts new file mode 100644 index 0000000000..377b6c9e09 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & + ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async (options) => { + const throwOnError = options.throwOnError ?? _config.throwOnError; + const responseStyle = options.responseStyle ?? _config.responseStyle; + + let request: Request | undefined; + let response: Response | undefined; + + try { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + + response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + throw jsonError ?? textError; + } catch (error) { + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); + } + } + + finalError = finalError || {}; + + if (throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response, + }; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/index.ts new file mode 100644 index 0000000000..b295edeca0 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/types.gen.ts new file mode 100644 index 0000000000..4b288a5099 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/types.gen.ts @@ -0,0 +1,217 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + /** request may be undefined, because error may be from building the request object itself */ + request?: Request; + /** response may be undefined, because error may be from building the request object itself or from a network error */ + response?: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts new file mode 100644 index 0000000000..eb0164fa8c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts @@ -0,0 +1,318 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + /** response may be undefined due to a network error where no response object is produced */ + response: Res | undefined, + /** request may be undefined, because error may be from building the request object itself */ + request: Req | undefined, + options: Options, +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/auth.gen.ts new file mode 100644 index 0000000000..3ebf994788 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/bodySerializer.gen.ts new file mode 100644 index 0000000000..67daca60f8 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/params.gen.ts new file mode 100644 index 0000000000..7955601a5c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/pathSerializer.gen.ts new file mode 100644 index 0000000000..994b2848c6 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000000..5000df606f --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts new file mode 100644 index 0000000000..ddf3c4d13a --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/types.gen.ts new file mode 100644 index 0000000000..9efe71d4c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/utils.gen.ts new file mode 100644 index 0000000000..9a4fec7830 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { type Options, postFoo } from './sdk.gen'; +export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/sdk.gen.ts new file mode 100644 index 0000000000..2a0fe9d412 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/sdk.gen.ts @@ -0,0 +1,34 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod'; + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { PostFooData, PostFooResponses } from './types.gen'; +import { zPostFooResponse } from './zod.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +export const postFoo = (options?: Options) => (options?.client ?? client).post({ + requestValidator: async (data) => await z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.never().optional() + }).parseAsync(data), + responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), + responseValidator: async (data) => await zPostFooResponse.parseAsync(data), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/types.gen.ts new file mode 100644 index 0000000000..318823eb66 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/types.gen.ts @@ -0,0 +1,36 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type TypeID = `${T}_${string}`; + +export type UserId = TypeID<'user'>; + +export type Foo = { + bar?: number; + foo: number; + id: UserId; +}; + +export type Bar = { + foo: number; + [key: string]: number; +}; + +export type PostFooData = { + body?: never; + path?: never; + query?: never; + url: '/foo'; +}; + +export type PostFooResponses = { + /** + * OK + */ + 200: Foo; +}; + +export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/zod.gen.ts new file mode 100644 index 0000000000..6173cb8206 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/zod.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod'; + +export const zFoo = z.object({ + bar: z.int().optional(), + foo: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).default(BigInt(0)), + id: z.string() +}); + +export const zBar = z.object({ + foo: z.int() +}); + +/** + * OK + */ +export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client.gen.ts new file mode 100644 index 0000000000..cab3c70195 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts new file mode 100644 index 0000000000..377b6c9e09 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & + ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async (options) => { + const throwOnError = options.throwOnError ?? _config.throwOnError; + const responseStyle = options.responseStyle ?? _config.responseStyle; + + let request: Request | undefined; + let response: Response | undefined; + + try { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + + response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + throw jsonError ?? textError; + } catch (error) { + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); + } + } + + finalError = finalError || {}; + + if (throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response, + }; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/index.ts new file mode 100644 index 0000000000..b295edeca0 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/types.gen.ts new file mode 100644 index 0000000000..4b288a5099 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/types.gen.ts @@ -0,0 +1,217 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + /** request may be undefined, because error may be from building the request object itself */ + request?: Request; + /** response may be undefined, because error may be from building the request object itself or from a network error */ + response?: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts new file mode 100644 index 0000000000..eb0164fa8c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts @@ -0,0 +1,318 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + /** response may be undefined due to a network error where no response object is produced */ + response: Res | undefined, + /** request may be undefined, because error may be from building the request object itself */ + request: Req | undefined, + options: Options, +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/auth.gen.ts new file mode 100644 index 0000000000..3ebf994788 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/bodySerializer.gen.ts new file mode 100644 index 0000000000..67daca60f8 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/params.gen.ts new file mode 100644 index 0000000000..7955601a5c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/pathSerializer.gen.ts new file mode 100644 index 0000000000..994b2848c6 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000000..5000df606f --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/serverSentEvents.gen.ts new file mode 100644 index 0000000000..ddf3c4d13a --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/types.gen.ts new file mode 100644 index 0000000000..9efe71d4c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/utils.gen.ts new file mode 100644 index 0000000000..9a4fec7830 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { type Options, postFoo } from './sdk.gen'; +export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/sdk.gen.ts new file mode 100644 index 0000000000..f741139140 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/sdk.gen.ts @@ -0,0 +1,34 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { PostFooData, PostFooResponses } from './types.gen'; +import { zPostFooResponse } from './zod.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +export const postFoo = (options?: Options) => (options?.client ?? client).post({ + requestValidator: async (data) => await z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.never()) + }).parseAsync(data), + responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), + responseValidator: async (data) => await zPostFooResponse.parseAsync(data), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/types.gen.ts new file mode 100644 index 0000000000..318823eb66 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/types.gen.ts @@ -0,0 +1,36 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type TypeID = `${T}_${string}`; + +export type UserId = TypeID<'user'>; + +export type Foo = { + bar?: number; + foo: number; + id: UserId; +}; + +export type Bar = { + foo: number; + [key: string]: number; +}; + +export type PostFooData = { + body?: never; + path?: never; + query?: never; + url: '/foo'; +}; + +export type PostFooResponses = { + /** + * OK + */ + 200: Foo; +}; + +export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/zod.gen.ts new file mode 100644 index 0000000000..4820089703 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/zod.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +export const zFoo = z.object({ + bar: z.optional(z.int()), + foo: z._default(z.coerce.bigint().check(z.minimum(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }), z.maximum(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' })), BigInt(0)), + id: z.string() +}); + +export const zBar = z.object({ + foo: z.int() +}); + +/** + * OK + */ +export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client.gen.ts new file mode 100644 index 0000000000..cab3c70195 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts new file mode 100644 index 0000000000..377b6c9e09 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & + ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async (options) => { + const throwOnError = options.throwOnError ?? _config.throwOnError; + const responseStyle = options.responseStyle ?? _config.responseStyle; + + let request: Request | undefined; + let response: Response | undefined; + + try { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + + response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + throw jsonError ?? textError; + } catch (error) { + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); + } + } + + finalError = finalError || {}; + + if (throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response, + }; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/index.ts new file mode 100644 index 0000000000..b295edeca0 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/types.gen.ts new file mode 100644 index 0000000000..4b288a5099 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/types.gen.ts @@ -0,0 +1,217 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + /** request may be undefined, because error may be from building the request object itself */ + request?: Request; + /** response may be undefined, because error may be from building the request object itself or from a network error */ + response?: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts new file mode 100644 index 0000000000..eb0164fa8c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts @@ -0,0 +1,318 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + /** response may be undefined due to a network error where no response object is produced */ + response: Res | undefined, + /** request may be undefined, because error may be from building the request object itself */ + request: Req | undefined, + options: Options, +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/auth.gen.ts new file mode 100644 index 0000000000..3ebf994788 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/bodySerializer.gen.ts new file mode 100644 index 0000000000..67daca60f8 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/params.gen.ts new file mode 100644 index 0000000000..7955601a5c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/pathSerializer.gen.ts new file mode 100644 index 0000000000..994b2848c6 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000000..5000df606f --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/serverSentEvents.gen.ts new file mode 100644 index 0000000000..ddf3c4d13a --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/types.gen.ts new file mode 100644 index 0000000000..9efe71d4c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/utils.gen.ts new file mode 100644 index 0000000000..9a4fec7830 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { type Options, postFoo } from './sdk.gen'; +export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/sdk.gen.ts new file mode 100644 index 0000000000..f95fb60307 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/sdk.gen.ts @@ -0,0 +1,34 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { PostFooData, PostFooResponses } from './types.gen'; +import { zPostFooResponse } from './zod.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +export const postFoo = (options?: Options) => (options?.client ?? client).post({ + requestValidator: async (data) => await z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.never().optional() + }).parseAsync(data), + responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), + responseValidator: async (data) => await zPostFooResponse.parseAsync(data), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/types.gen.ts new file mode 100644 index 0000000000..318823eb66 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/types.gen.ts @@ -0,0 +1,36 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type TypeID = `${T}_${string}`; + +export type UserId = TypeID<'user'>; + +export type Foo = { + bar?: number; + foo: number; + id: UserId; +}; + +export type Bar = { + foo: number; + [key: string]: number; +}; + +export type PostFooData = { + body?: never; + path?: never; + query?: never; + url: '/foo'; +}; + +export type PostFooResponses = { + /** + * OK + */ + 200: Foo; +}; + +export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/zod.gen.ts new file mode 100644 index 0000000000..01c7ccbc5b --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/zod.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +export const zFoo = z.object({ + bar: z.number().int().optional(), + foo: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).default(BigInt(0)), + id: z.string() +}); + +export const zBar = z.object({ + foo: z.number().int() +}); + +/** + * OK + */ +export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client.gen.ts new file mode 100644 index 0000000000..cab3c70195 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts new file mode 100644 index 0000000000..377b6c9e09 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts @@ -0,0 +1,280 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & + ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async (options) => { + const throwOnError = options.throwOnError ?? _config.throwOnError; + const responseStyle = options.responseStyle ?? _config.responseStyle; + + let request: Request | undefined; + let response: Response | undefined; + + try { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + + response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + throw jsonError ?? textError; + } catch (error) { + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); + } + } + + finalError = finalError || {}; + + if (throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response, + }; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/index.ts new file mode 100644 index 0000000000..b295edeca0 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/types.gen.ts new file mode 100644 index 0000000000..4b288a5099 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/types.gen.ts @@ -0,0 +1,217 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + /** request may be undefined, because error may be from building the request object itself */ + request?: Request; + /** response may be undefined, because error may be from building the request object itself or from a network error */ + response?: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts new file mode 100644 index 0000000000..eb0164fa8c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts @@ -0,0 +1,318 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + /** response may be undefined due to a network error where no response object is produced */ + response: Res | undefined, + /** request may be undefined, because error may be from building the request object itself */ + request: Req | undefined, + options: Options, +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/auth.gen.ts new file mode 100644 index 0000000000..3ebf994788 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/bodySerializer.gen.ts new file mode 100644 index 0000000000..67daca60f8 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/params.gen.ts new file mode 100644 index 0000000000..7955601a5c --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/pathSerializer.gen.ts new file mode 100644 index 0000000000..994b2848c6 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000000..5000df606f --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/serverSentEvents.gen.ts new file mode 100644 index 0000000000..ddf3c4d13a --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/types.gen.ts new file mode 100644 index 0000000000..9efe71d4c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/utils.gen.ts new file mode 100644 index 0000000000..9a4fec7830 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { type Options, postFoo } from './sdk.gen'; +export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/sdk.gen.ts new file mode 100644 index 0000000000..2a0fe9d412 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/sdk.gen.ts @@ -0,0 +1,34 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod'; + +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { PostFooData, PostFooResponses } from './types.gen'; +import { zPostFooResponse } from './zod.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +export const postFoo = (options?: Options) => (options?.client ?? client).post({ + requestValidator: async (data) => await z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.never().optional() + }).parseAsync(data), + responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), + responseValidator: async (data) => await zPostFooResponse.parseAsync(data), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/types.gen.ts new file mode 100644 index 0000000000..318823eb66 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/types.gen.ts @@ -0,0 +1,36 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type TypeID = `${T}_${string}`; + +export type UserId = TypeID<'user'>; + +export type Foo = { + bar?: number; + foo: number; + id: UserId; +}; + +export type Bar = { + foo: number; + [key: string]: number; +}; + +export type PostFooData = { + body?: never; + path?: never; + query?: never; + url: '/foo'; +}; + +export type PostFooResponses = { + /** + * OK + */ + 200: Foo; +}; + +export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/zod.gen.ts new file mode 100644 index 0000000000..6173cb8206 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/zod.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod'; + +export const zFoo = z.object({ + bar: z.int().optional(), + foo: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).default(BigInt(0)), + id: z.string() +}); + +export const zBar = z.object({ + foo: z.int() +}); + +/** + * OK + */ +export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts b/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts index cfc2a9f2ee..3b2c12902f 100644 --- a/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts @@ -55,6 +55,25 @@ for (const version of versions) { }), description: 'handles various schema types and formats', }, + { + config: createConfig({ + input: 'type-format.yaml', + output: 'zod-transformer', + plugins: [ + '@hey-api/client-fetch', + { + compatibilityVersion: zodVersion.compatibilityVersion, + name: 'zod', + }, + { + name: '@hey-api/sdk', + transformer: 'zod', + validator: true, + }, + ], + }), + description: 'uses zod as response transformer', + }, ]; it.each(scenarios)('$description', async ({ config }) => { From 5a4d8dbd2505fb630189b571c329724fbf281720 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 3 May 2026 23:42:33 +0800 Subject: [PATCH 05/14] docs(zod): document that zod as transformer is a thing now --- docs/openapi-ts/plugins/transformers.md | 8 ++++ docs/openapi-ts/plugins/zod.md | 49 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/docs/openapi-ts/plugins/transformers.md b/docs/openapi-ts/plugins/transformers.md index 08e6e34bd3..99fff50b22 100644 --- a/docs/openapi-ts/plugins/transformers.md +++ b/docs/openapi-ts/plugins/transformers.md @@ -3,6 +3,14 @@ title: Transformers description: Learn about transforming data with @hey-api/openapi-ts. --- +::: tip Using Zod instead of `@hey-api/transformers` +This page covers the `@hey-api/transformers` plugin, which provides a built-in way to transform response data. + +If you're already using the Zod plugin for validation, you may also use Zod as your response transformer. This lets Zod's `parseAsync` handle response transformation directly. + +See the [Zod plugin docs](/openapi-ts/plugins/zod#zod-as-a-response-transformer) for details. +::: + # Transformers JSON is the most commonly used data format in REST APIs. However, it does not map well to complex data types. For example, both regular strings and date strings become simple strings in JSON. diff --git a/docs/openapi-ts/plugins/zod.md b/docs/openapi-ts/plugins/zod.md index 7c471ceef9..fb243504fd 100644 --- a/docs/openapi-ts/plugins/zod.md +++ b/docs/openapi-ts/plugins/zod.md @@ -62,6 +62,55 @@ export default { Learn more about data validators in your SDKs on the [SDKs](/openapi-ts/plugins/sdk#validators) page. +### Zod as a response transformer + +You can also use Zod as a **response transformer** by setting `sdk.transformer` to `'zod'`. This causes the SDK to call `parseAsync` on the response and use its return value, so Zod's coercion and transformation rules are applied before the data is returned to you. + +```js +export default { + input: 'hey-api/backend', // sign up at app.heyapi.dev + output: 'src/client', + plugins: [ + // ...other plugins + 'zod', + { + name: '@hey-api/sdk', // [!code ++] + transformer: 'zod', // [!code ++] + }, + ], +}; +``` + +This generates an inline `responseTransformer` in each SDK function: + +```ts +// sdk.gen.ts (generated) +export const getFoo = (options?) => + client.get({ + responseTransformer: async (data) => await zGetFooResponse.parseAsync(data), + url: '/foo', + ...options, + }); +``` + +::: warning Avoid duplicate `parseAsync` calls +When `transformer: 'zod'` is enabled, `parseAsync` is already called on the response as part of transformation. If you also enable `validator: true` (or `validator: { response: true }`), `parseAsync` will be called a **second time** for response validation, which is redundant and adds unnecessary overhead. + +To avoid this, disable response validation when using Zod as a transformer: + +```js +{ + name: '@hey-api/sdk', + transformer: 'zod', // [!code ++] + validator: { // [!code ++] + request: true, // still validate outgoing requests // [!code ++] + response: false, // skip — transformer already runs parseAsync // [!code ++] + }, // [!code ++] +} +``` + +::: + ## Output The Zod plugin will generate the following artifacts, depending on the input specification. From d9b646f4beb1e693006f3e8a577fd186097e7813 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 16 May 2026 00:39:59 +0800 Subject: [PATCH 06/14] test: update snapshots --- .../mini/zod-transformer/client/client.gen.ts | 5 +---- .../2.0.x/mini/zod-transformer/client/utils.gen.ts | 14 ++++++-------- .../2.0.x/v3/zod-transformer/client/client.gen.ts | 5 +---- .../2.0.x/v3/zod-transformer/client/utils.gen.ts | 14 ++++++-------- .../2.0.x/v4/zod-transformer/client/client.gen.ts | 5 +---- .../2.0.x/v4/zod-transformer/client/utils.gen.ts | 14 ++++++-------- .../mini/zod-transformer/client/client.gen.ts | 5 +---- .../3.0.x/mini/zod-transformer/client/utils.gen.ts | 14 ++++++-------- .../3.0.x/v3/zod-transformer/client/client.gen.ts | 5 +---- .../3.0.x/v3/zod-transformer/client/utils.gen.ts | 14 ++++++-------- .../3.0.x/v4/zod-transformer/client/client.gen.ts | 5 +---- .../3.0.x/v4/zod-transformer/client/utils.gen.ts | 14 ++++++-------- .../mini/zod-transformer/client/client.gen.ts | 5 +---- .../3.1.x/mini/zod-transformer/client/utils.gen.ts | 14 ++++++-------- .../3.1.x/v3/zod-transformer/client/client.gen.ts | 5 +---- .../3.1.x/v3/zod-transformer/client/utils.gen.ts | 14 ++++++-------- .../3.1.x/v4/zod-transformer/client/client.gen.ts | 5 +---- .../3.1.x/v4/zod-transformer/client/utils.gen.ts | 14 ++++++-------- 18 files changed, 63 insertions(+), 108 deletions(-) diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/client.gen.ts index 377b6c9e09..fc3f037f16 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/client.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/client.gen.ts @@ -48,10 +48,7 @@ export const createClient = (config: Config = {}): Client => { }; if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); + await setAuthParams(opts); } if (opts.requestValidator) { diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/utils.gen.ts index eb0164fa8c..7800fe4b9d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/utils.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/utils.gen.ts @@ -118,14 +118,12 @@ const checkForExistence = ( return false; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { +export async function setAuthParams( + options: Pick & { headers: Headers; - }) => { - for (const auth of security) { + }, +): Promise { + for (const auth of options.security ?? []) { if (checkForExistence(options, auth.name)) { continue; } @@ -154,7 +152,7 @@ export const setAuthParams = async ({ break; } } -}; +} export const buildUrl: Client['buildUrl'] = (options) => getUrl({ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/client.gen.ts index 377b6c9e09..fc3f037f16 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/client.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/client.gen.ts @@ -48,10 +48,7 @@ export const createClient = (config: Config = {}): Client => { }; if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); + await setAuthParams(opts); } if (opts.requestValidator) { diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/utils.gen.ts index eb0164fa8c..7800fe4b9d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/utils.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/utils.gen.ts @@ -118,14 +118,12 @@ const checkForExistence = ( return false; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { +export async function setAuthParams( + options: Pick & { headers: Headers; - }) => { - for (const auth of security) { + }, +): Promise { + for (const auth of options.security ?? []) { if (checkForExistence(options, auth.name)) { continue; } @@ -154,7 +152,7 @@ export const setAuthParams = async ({ break; } } -}; +} export const buildUrl: Client['buildUrl'] = (options) => getUrl({ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/client.gen.ts index 377b6c9e09..fc3f037f16 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/client.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/client.gen.ts @@ -48,10 +48,7 @@ export const createClient = (config: Config = {}): Client => { }; if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); + await setAuthParams(opts); } if (opts.requestValidator) { diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/utils.gen.ts index eb0164fa8c..7800fe4b9d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/utils.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/utils.gen.ts @@ -118,14 +118,12 @@ const checkForExistence = ( return false; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { +export async function setAuthParams( + options: Pick & { headers: Headers; - }) => { - for (const auth of security) { + }, +): Promise { + for (const auth of options.security ?? []) { if (checkForExistence(options, auth.name)) { continue; } @@ -154,7 +152,7 @@ export const setAuthParams = async ({ break; } } -}; +} export const buildUrl: Client['buildUrl'] = (options) => getUrl({ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts index 377b6c9e09..fc3f037f16 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts @@ -48,10 +48,7 @@ export const createClient = (config: Config = {}): Client => { }; if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); + await setAuthParams(opts); } if (opts.requestValidator) { diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts index eb0164fa8c..7800fe4b9d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts @@ -118,14 +118,12 @@ const checkForExistence = ( return false; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { +export async function setAuthParams( + options: Pick & { headers: Headers; - }) => { - for (const auth of security) { + }, +): Promise { + for (const auth of options.security ?? []) { if (checkForExistence(options, auth.name)) { continue; } @@ -154,7 +152,7 @@ export const setAuthParams = async ({ break; } } -}; +} export const buildUrl: Client['buildUrl'] = (options) => getUrl({ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts index 377b6c9e09..fc3f037f16 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts @@ -48,10 +48,7 @@ export const createClient = (config: Config = {}): Client => { }; if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); + await setAuthParams(opts); } if (opts.requestValidator) { diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts index eb0164fa8c..7800fe4b9d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts @@ -118,14 +118,12 @@ const checkForExistence = ( return false; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { +export async function setAuthParams( + options: Pick & { headers: Headers; - }) => { - for (const auth of security) { + }, +): Promise { + for (const auth of options.security ?? []) { if (checkForExistence(options, auth.name)) { continue; } @@ -154,7 +152,7 @@ export const setAuthParams = async ({ break; } } -}; +} export const buildUrl: Client['buildUrl'] = (options) => getUrl({ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts index 377b6c9e09..fc3f037f16 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts @@ -48,10 +48,7 @@ export const createClient = (config: Config = {}): Client => { }; if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); + await setAuthParams(opts); } if (opts.requestValidator) { diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts index eb0164fa8c..7800fe4b9d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts @@ -118,14 +118,12 @@ const checkForExistence = ( return false; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { +export async function setAuthParams( + options: Pick & { headers: Headers; - }) => { - for (const auth of security) { + }, +): Promise { + for (const auth of options.security ?? []) { if (checkForExistence(options, auth.name)) { continue; } @@ -154,7 +152,7 @@ export const setAuthParams = async ({ break; } } -}; +} export const buildUrl: Client['buildUrl'] = (options) => getUrl({ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts index 377b6c9e09..fc3f037f16 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts @@ -48,10 +48,7 @@ export const createClient = (config: Config = {}): Client => { }; if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); + await setAuthParams(opts); } if (opts.requestValidator) { diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts index eb0164fa8c..7800fe4b9d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts @@ -118,14 +118,12 @@ const checkForExistence = ( return false; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { +export async function setAuthParams( + options: Pick & { headers: Headers; - }) => { - for (const auth of security) { + }, +): Promise { + for (const auth of options.security ?? []) { if (checkForExistence(options, auth.name)) { continue; } @@ -154,7 +152,7 @@ export const setAuthParams = async ({ break; } } -}; +} export const buildUrl: Client['buildUrl'] = (options) => getUrl({ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts index 377b6c9e09..fc3f037f16 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts @@ -48,10 +48,7 @@ export const createClient = (config: Config = {}): Client => { }; if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); + await setAuthParams(opts); } if (opts.requestValidator) { diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts index eb0164fa8c..7800fe4b9d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts @@ -118,14 +118,12 @@ const checkForExistence = ( return false; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { +export async function setAuthParams( + options: Pick & { headers: Headers; - }) => { - for (const auth of security) { + }, +): Promise { + for (const auth of options.security ?? []) { if (checkForExistence(options, auth.name)) { continue; } @@ -154,7 +152,7 @@ export const setAuthParams = async ({ break; } } -}; +} export const buildUrl: Client['buildUrl'] = (options) => getUrl({ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts index 377b6c9e09..fc3f037f16 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts @@ -48,10 +48,7 @@ export const createClient = (config: Config = {}): Client => { }; if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); + await setAuthParams(opts); } if (opts.requestValidator) { diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts index eb0164fa8c..7800fe4b9d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts @@ -118,14 +118,12 @@ const checkForExistence = ( return false; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { +export async function setAuthParams( + options: Pick & { headers: Headers; - }) => { - for (const auth of security) { + }, +): Promise { + for (const auth of options.security ?? []) { if (checkForExistence(options, auth.name)) { continue; } @@ -154,7 +152,7 @@ export const setAuthParams = async ({ break; } } -}; +} export const buildUrl: Client['buildUrl'] = (options) => getUrl({ From 4277623fd8ac46ac43f3a18f3807f22def7439f5 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 16 May 2026 00:48:16 +0800 Subject: [PATCH 07/14] chore: update sdk types --- .../src/plugins/@hey-api/sdk/types.ts | 87 ++++--------------- packages/openapi-ts/src/plugins/types.ts | 2 +- packages/openapi-ts/src/plugins/zod/config.ts | 2 +- 3 files changed, 20 insertions(+), 71 deletions(-) diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/types.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.ts index b8da559cb2..fc36d2f087 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/types.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.ts @@ -76,33 +76,28 @@ export type UserConfig = Plugin.Name<'@hey-api/sdk'> & * convert for example ISO strings into Date objects. However, transformation * adds runtime overhead, so it's not recommended to use unless necessary. * - * You can customize the selected transformer output through its plugin. You - * can also set `transformer` to `true` to automatically choose the - * transformer from your defined plugins. - * - * When set to `'zod'`, the Zod plugin's `parseAsync` is used as the - * response transformer, which applies Zod's coercion and transformation - * rules (e.g., converting strings to `Date` objects via `z.coerce.date()`). - * This requires the `zod` plugin to be configured in your plugins. + * You can customize the transformer output through its plugin. You can also + * set `transformer` to `true` to automatically choose the transformer from your + * defined plugins. * - * Use the object form for fine-grained control over response transformation. + * Ensure you have declared the selected library as a dependency to avoid + * errors. * * @default false */ transformer?: | PluginTransformerNames - | 'zod' | boolean | { /** - * Transform response data using the specified transformer. + * Transform response data before returning. * - * Can be a transformer plugin name, `'zod'`, or a boolean (`true` to - * auto-select, `false` to disable). + * Can be a transformer plugin name or boolean (true to auto-select, false + * to disable). * * @default false */ - response?: PluginTransformerNames | 'zod' | boolean; + response?: PluginTransformerNames | boolean; }; /** * Validate request and/or response data against schema before returning. @@ -212,45 +207,19 @@ export type UserConfig = Plugin.Name<'@hey-api/sdk'> & response?: 'body' | 'response'; }; -/** Normalized configuration */ export type Config = Plugin.Name<'@hey-api/sdk'> & Plugin.Hooks & Plugin.Comments & Plugin.Exports & { - /** - * Should the generated functions contain auth mechanisms? You may want to - * disable this option if you're handling auth yourself or defining it - * globally on the client and want to reduce the size of generated code. - * - * @default true - */ + /** Should the generated functions contain auth mechanisms? */ auth: boolean; - /** - * Use an internal client instance to send HTTP requests? This is useful if - * you don't want to manually pass the client to each SDK function. - * - * You can customize the selected client output through its plugin. You can - * also set `client` to `true` to automatically choose the client from your - * defined plugins. If we can't detect a client plugin when using `true`, we - * will default to `@hey-api/client-fetch`. - * - * @default true - */ + /** Use an internal client instance to send HTTP requests? */ client: PluginClientNames | false; /** Configuration for generating SDK code examples. */ examples: ExamplesConfig; /** Define the structure of generated SDK operations. */ operations: OperationsConfig; - /** - * Define how request parameters are structured in generated SDK methods. - * - * - `'flat'` merges parameters into a single object. - * - `'grouped'` separates parameters by transport layer. - * - * Use `'flat'` for simpler calls or `'grouped'` for stricter typing and code clarity. - * - * @default 'grouped' - */ + /** Define how request parameters are structured in generated SDK methods. */ paramsStructure: 'flat' | 'grouped'; /** * **This feature works only with the Fetch client** @@ -260,36 +229,16 @@ export type Config = Plugin.Name<'@hey-api/sdk'> & * @default 'fields' */ responseStyle: 'data' | 'fields'; - /** - * Configuration for response data transformation. - */ + /** Transform response data before returning. */ transformer: { - /** - * The transformer plugin to use for response transformation, or false to - * disable. - * - * @default false - */ - response: PluginTransformerNames | 'zod' | false; + /** The transformer plugin to use for response transformation, or false to disable. */ + response: PluginTransformerNames | false; }; - /** - * Validate request and/or response data against schema before returning. - * This is useful if you want to ensure the request and/or response conforms - * to a desired shape. However, validation adds runtime overhead, so it's - * not recommended to use unless absolutely necessary. - */ + /** Validate request and/or response data against schema before returning. */ validator: { - /** - * The validator plugin to use for request validation, or false to disable. - * - * @default false - */ + /** The validator plugin to use for request validation, or false to disable. */ request: PluginValidatorNames | false; - /** - * The validator plugin to use for response validation, or false to disable. - * - * @default false - */ + /** The validator plugin to use for response validation, or false to disable. */ response: PluginValidatorNames | false; }; diff --git a/packages/openapi-ts/src/plugins/types.ts b/packages/openapi-ts/src/plugins/types.ts index 4da1edded1..3a1d22f1d2 100644 --- a/packages/openapi-ts/src/plugins/types.ts +++ b/packages/openapi-ts/src/plugins/types.ts @@ -9,6 +9,6 @@ export type PluginClientNames = export type PluginMockNames = '@faker-js/faker'; -export type PluginTransformerNames = '@hey-api/transformers'; +export type PluginTransformerNames = '@hey-api/transformers' | 'zod'; export type PluginValidatorNames = 'arktype' | 'valibot' | 'zod'; diff --git a/packages/openapi-ts/src/plugins/zod/config.ts b/packages/openapi-ts/src/plugins/zod/config.ts index 7941915c1b..1ea5b856a9 100644 --- a/packages/openapi-ts/src/plugins/zod/config.ts +++ b/packages/openapi-ts/src/plugins/zod/config.ts @@ -1009,7 +1009,7 @@ export const defaultConfig: ZodPlugin['Config'] = { value: plugin.config.webhooks, }); }, - tags: ['validator'], + tags: ['transformer', 'validator'], }; /** From 186ed9e615885521e6b65974a03767e756ddc6b5 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 16 May 2026 01:25:41 +0800 Subject: [PATCH 08/14] test: update snapshots --- .../2.0.x/mini/zod-transformer/types.gen.ts | 36 -- .../2.0.x/v3/zod-transformer/types.gen.ts | 36 -- .../2.0.x/v4/zod-transformer/types.gen.ts | 36 -- .../3.0.x/mini/zod-transformer/client.gen.ts | 16 - .../mini/zod-transformer/client/client.gen.ts | 277 --------------- .../mini/zod-transformer/client/index.ts | 25 -- .../mini/zod-transformer/client/types.gen.ts | 217 ------------ .../mini/zod-transformer/client/utils.gen.ts | 316 ------------------ .../mini/zod-transformer/core/auth.gen.ts | 41 --- .../core/bodySerializer.gen.ts | 82 ----- .../mini/zod-transformer/core/params.gen.ts | 169 ---------- .../core/pathSerializer.gen.ts | 171 ---------- .../core/queryKeySerializer.gen.ts | 117 ------- .../core/serverSentEvents.gen.ts | 242 -------------- .../mini/zod-transformer/core/types.gen.ts | 104 ------ .../mini/zod-transformer/core/utils.gen.ts | 140 -------- .../3.0.x/mini/zod-transformer/index.ts | 4 - .../3.0.x/mini/zod-transformer/sdk.gen.ts | 34 -- .../3.0.x/mini/zod-transformer/zod.gen.ts | 18 - .../3.0.x/v3/zod-transformer/client.gen.ts | 16 - .../v3/zod-transformer/client/client.gen.ts | 277 --------------- .../3.0.x/v3/zod-transformer/client/index.ts | 25 -- .../v3/zod-transformer/client/types.gen.ts | 217 ------------ .../v3/zod-transformer/client/utils.gen.ts | 316 ------------------ .../3.0.x/v3/zod-transformer/core/auth.gen.ts | 41 --- .../core/bodySerializer.gen.ts | 82 ----- .../v3/zod-transformer/core/params.gen.ts | 169 ---------- .../core/pathSerializer.gen.ts | 171 ---------- .../core/queryKeySerializer.gen.ts | 117 ------- .../core/serverSentEvents.gen.ts | 242 -------------- .../v3/zod-transformer/core/types.gen.ts | 104 ------ .../v3/zod-transformer/core/utils.gen.ts | 140 -------- .../3.0.x/v3/zod-transformer/index.ts | 4 - .../3.0.x/v3/zod-transformer/sdk.gen.ts | 34 -- .../3.0.x/v3/zod-transformer/zod.gen.ts | 18 - .../3.0.x/v4/zod-transformer/client.gen.ts | 16 - .../v4/zod-transformer/client/client.gen.ts | 277 --------------- .../3.0.x/v4/zod-transformer/client/index.ts | 25 -- .../v4/zod-transformer/client/types.gen.ts | 217 ------------ .../v4/zod-transformer/client/utils.gen.ts | 316 ------------------ .../3.0.x/v4/zod-transformer/core/auth.gen.ts | 41 --- .../core/bodySerializer.gen.ts | 82 ----- .../v4/zod-transformer/core/params.gen.ts | 169 ---------- .../core/pathSerializer.gen.ts | 171 ---------- .../core/queryKeySerializer.gen.ts | 117 ------- .../core/serverSentEvents.gen.ts | 242 -------------- .../v4/zod-transformer/core/types.gen.ts | 104 ------ .../v4/zod-transformer/core/utils.gen.ts | 140 -------- .../3.0.x/v4/zod-transformer/index.ts | 4 - .../3.0.x/v4/zod-transformer/sdk.gen.ts | 34 -- .../3.0.x/v4/zod-transformer/zod.gen.ts | 18 - .../mini/transformer}/client.gen.ts | 0 .../mini/transformer}/client/client.gen.ts | 0 .../mini/transformer}/client/index.ts | 0 .../mini/transformer}/client/types.gen.ts | 0 .../mini/transformer}/client/utils.gen.ts | 0 .../mini/transformer}/core/auth.gen.ts | 0 .../transformer}/core/bodySerializer.gen.ts | 0 .../mini/transformer}/core/params.gen.ts | 0 .../transformer}/core/pathSerializer.gen.ts | 0 .../core/queryKeySerializer.gen.ts | 0 .../transformer}/core/serverSentEvents.gen.ts | 0 .../mini/transformer}/core/types.gen.ts | 0 .../mini/transformer}/core/utils.gen.ts | 0 .../mini/transformer}/index.ts | 0 .../mini/transformer}/sdk.gen.ts | 0 .../mini/transformer}/types.gen.ts | 0 .../mini/transformer}/zod.gen.ts | 0 .../3.1.x/mini/zod-transformer/client.gen.ts | 16 - .../mini/zod-transformer/client/client.gen.ts | 277 --------------- .../mini/zod-transformer/client/index.ts | 25 -- .../mini/zod-transformer/client/types.gen.ts | 217 ------------ .../mini/zod-transformer/client/utils.gen.ts | 316 ------------------ .../mini/zod-transformer/core/auth.gen.ts | 41 --- .../core/bodySerializer.gen.ts | 82 ----- .../mini/zod-transformer/core/params.gen.ts | 169 ---------- .../core/pathSerializer.gen.ts | 171 ---------- .../core/queryKeySerializer.gen.ts | 117 ------- .../core/serverSentEvents.gen.ts | 242 -------------- .../mini/zod-transformer/core/types.gen.ts | 104 ------ .../mini/zod-transformer/core/utils.gen.ts | 140 -------- .../3.1.x/mini/zod-transformer/index.ts | 4 - .../3.1.x/mini/zod-transformer/sdk.gen.ts | 34 -- .../3.1.x/mini/zod-transformer/types.gen.ts | 36 -- .../3.1.x/mini/zod-transformer/zod.gen.ts | 18 - .../v3/transformer}/client.gen.ts | 0 .../v3/transformer}/client/client.gen.ts | 0 .../v3/transformer}/client/index.ts | 0 .../v3/transformer}/client/types.gen.ts | 0 .../v3/transformer}/client/utils.gen.ts | 0 .../v3/transformer}/core/auth.gen.ts | 0 .../transformer}/core/bodySerializer.gen.ts | 0 .../v3/transformer}/core/params.gen.ts | 0 .../transformer}/core/pathSerializer.gen.ts | 0 .../core/queryKeySerializer.gen.ts | 0 .../transformer}/core/serverSentEvents.gen.ts | 0 .../v3/transformer}/core/types.gen.ts | 0 .../v3/transformer}/core/utils.gen.ts | 0 .../v3/transformer}/index.ts | 0 .../v3/transformer}/sdk.gen.ts | 0 .../v3/transformer}/types.gen.ts | 0 .../v3/transformer}/zod.gen.ts | 0 .../3.1.x/v3/zod-transformer/client.gen.ts | 16 - .../v3/zod-transformer/client/client.gen.ts | 277 --------------- .../3.1.x/v3/zod-transformer/client/index.ts | 25 -- .../v3/zod-transformer/client/types.gen.ts | 217 ------------ .../v3/zod-transformer/client/utils.gen.ts | 316 ------------------ .../3.1.x/v3/zod-transformer/core/auth.gen.ts | 41 --- .../core/bodySerializer.gen.ts | 82 ----- .../v3/zod-transformer/core/params.gen.ts | 169 ---------- .../core/pathSerializer.gen.ts | 171 ---------- .../core/queryKeySerializer.gen.ts | 117 ------- .../core/serverSentEvents.gen.ts | 242 -------------- .../v3/zod-transformer/core/types.gen.ts | 104 ------ .../v3/zod-transformer/core/utils.gen.ts | 140 -------- .../3.1.x/v3/zod-transformer/index.ts | 4 - .../3.1.x/v3/zod-transformer/sdk.gen.ts | 34 -- .../3.1.x/v3/zod-transformer/types.gen.ts | 36 -- .../3.1.x/v3/zod-transformer/zod.gen.ts | 18 - .../v4/transformer}/client.gen.ts | 0 .../v4/transformer}/client/client.gen.ts | 0 .../v4/transformer}/client/index.ts | 0 .../v4/transformer}/client/types.gen.ts | 0 .../v4/transformer}/client/utils.gen.ts | 0 .../v4/transformer}/core/auth.gen.ts | 0 .../transformer}/core/bodySerializer.gen.ts | 0 .../v4/transformer}/core/params.gen.ts | 0 .../transformer}/core/pathSerializer.gen.ts | 0 .../core/queryKeySerializer.gen.ts | 0 .../transformer}/core/serverSentEvents.gen.ts | 0 .../v4/transformer}/core/types.gen.ts | 0 .../v4/transformer}/core/utils.gen.ts | 0 .../v4/transformer}/index.ts | 0 .../v4/transformer}/sdk.gen.ts | 0 .../v4/transformer}/types.gen.ts | 0 .../v4/transformer}/zod.gen.ts | 0 .../3.1.x/v4/zod-transformer/client.gen.ts | 16 - .../v4/zod-transformer/client/client.gen.ts | 277 --------------- .../3.1.x/v4/zod-transformer/client/index.ts | 25 -- .../v4/zod-transformer/client/types.gen.ts | 217 ------------ .../v4/zod-transformer/client/utils.gen.ts | 316 ------------------ .../3.1.x/v4/zod-transformer/core/auth.gen.ts | 41 --- .../core/bodySerializer.gen.ts | 82 ----- .../v4/zod-transformer/core/params.gen.ts | 169 ---------- .../core/pathSerializer.gen.ts | 171 ---------- .../core/queryKeySerializer.gen.ts | 117 ------- .../core/serverSentEvents.gen.ts | 242 -------------- .../v4/zod-transformer/core/types.gen.ts | 104 ------ .../v4/zod-transformer/core/utils.gen.ts | 140 -------- .../3.1.x/v4/zod-transformer/index.ts | 4 - .../3.1.x/v4/zod-transformer/sdk.gen.ts | 34 -- .../3.1.x/v4/zod-transformer/types.gen.ts | 36 -- .../3.1.x/v4/zod-transformer/zod.gen.ts | 18 - .../zod/v4/test/3.1.x.test.ts | 18 + .../zod/v4/test/openapi.test.ts | 19 -- 155 files changed, 18 insertions(+), 12073 deletions(-) delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/auth.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/bodySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/params.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/pathSerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/sdk.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/zod.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/auth.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/bodySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/params.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/pathSerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/sdk.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/zod.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/auth.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/bodySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/params.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/pathSerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/sdk.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/zod.gen.ts rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/client.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/client/client.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/client/index.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/client/types.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/client/utils.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/core/auth.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/core/bodySerializer.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/core/params.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/core/pathSerializer.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/core/queryKeySerializer.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/core/serverSentEvents.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/core/types.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/core/utils.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/index.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/sdk.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{3.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/types.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/mini/zod-transformer => 3.1.x/mini/transformer}/zod.gen.ts (100%) delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/auth.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/bodySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/params.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/pathSerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/queryKeySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/serverSentEvents.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/sdk.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/zod.gen.ts rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/client.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/client/client.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/client/index.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/client/types.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/client/utils.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/core/auth.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/core/bodySerializer.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/core/params.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/core/pathSerializer.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/core/queryKeySerializer.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/core/serverSentEvents.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/core/types.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/core/utils.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/index.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/sdk.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{3.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/types.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v3/zod-transformer => 3.1.x/v3/transformer}/zod.gen.ts (100%) delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/auth.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/bodySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/params.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/pathSerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/queryKeySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/serverSentEvents.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/sdk.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/zod.gen.ts rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/client.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/client/client.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/client/index.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/client/types.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/client/utils.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/core/auth.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/core/bodySerializer.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/core/params.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/core/pathSerializer.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/core/queryKeySerializer.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/core/serverSentEvents.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/core/types.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/core/utils.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/index.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/sdk.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{3.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/types.gen.ts (100%) rename packages/openapi-ts-tests/zod/v4/__snapshots__/{2.0.x/v4/zod-transformer => 3.1.x/v4/transformer}/zod.gen.ts (100%) delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/auth.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/bodySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/params.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/pathSerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/queryKeySerializer.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/serverSentEvents.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/utils.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/index.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/sdk.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/types.gen.ts delete mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/zod.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/types.gen.ts deleted file mode 100644 index 9c2a6c4718..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/types.gen.ts +++ /dev/null @@ -1,36 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: string; -}; - -export type TypeID = `${T}_${string}`; - -export type UserId = TypeID<'user'>; - -export type Foo = { - bar?: number; - foo: number; - id: UserId; -}; - -export type Bar = { - foo: number; - [key: string]: number; -}; - -export type PostFooData = { - body?: never; - path?: never; - query?: never; - url: '/foo'; -}; - -export type PostFooResponses = { - /** - * OK - */ - 200: Foo; -}; - -export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/types.gen.ts deleted file mode 100644 index 9c2a6c4718..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/types.gen.ts +++ /dev/null @@ -1,36 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: string; -}; - -export type TypeID = `${T}_${string}`; - -export type UserId = TypeID<'user'>; - -export type Foo = { - bar?: number; - foo: number; - id: UserId; -}; - -export type Bar = { - foo: number; - [key: string]: number; -}; - -export type PostFooData = { - body?: never; - path?: never; - query?: never; - url: '/foo'; -}; - -export type PostFooResponses = { - /** - * OK - */ - 200: Foo; -}; - -export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/types.gen.ts deleted file mode 100644 index 9c2a6c4718..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/types.gen.ts +++ /dev/null @@ -1,36 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: string; -}; - -export type TypeID = `${T}_${string}`; - -export type UserId = TypeID<'user'>; - -export type Foo = { - bar?: number; - foo: number; - id: UserId; -}; - -export type Bar = { - foo: number; - [key: string]: number; -}; - -export type PostFooData = { - body?: never; - path?: never; - query?: never; - url: '/foo'; -}; - -export type PostFooResponses = { - /** - * OK - */ - 200: Foo; -}; - -export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client.gen.ts deleted file mode 100644 index cab3c70195..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client.gen.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from './client'; -import type { ClientOptions as ClientOptions2 } from './types.gen'; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = (override?: Config) => Config & T>; - -export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts deleted file mode 100644 index fc3f037f16..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/client.gen.ts +++ /dev/null @@ -1,277 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from '../core/serverSentEvents.gen'; -import type { HttpMethod } from '../core/types.gen'; -import { getValidRequestBody } from '../core/utils.gen'; -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors(); - - const beforeRequest = async < - TData = unknown, - TResponseStyle extends 'data' | 'fields' = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, - >( - options: RequestOptions, - ) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined as string | undefined, - }; - - if (opts.security) { - await setAuthParams(opts); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const resolvedOpts = opts as typeof opts & - ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - - return { opts: resolvedOpts, url }; - }; - - const request: Client['request'] = async (options) => { - const throwOnError = options.throwOnError ?? _config.throwOnError; - const responseStyle = options.responseStyle ?? _config.responseStyle; - - let request: Request | undefined; - let response: Response | undefined; - - try { - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: getValidRequestBody(opts), - }; - - request = new Request(url, requestInit); - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - - response = await _fetch(request); - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (response.status === 204 || response.headers.get('Content-Length') === '0') { - let emptyData: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'text': - emptyData = await response[parseAs](); - break; - case 'formData': - emptyData = new FormData(); - break; - case 'stream': - emptyData = response.body; - break; - case 'json': - default: - emptyData = {}; - break; - } - return opts.responseStyle === 'data' - ? emptyData - : { - data: emptyData, - ...result, - }; - } - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'text': - data = await response[parseAs](); - break; - case 'json': { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text(); - data = text ? JSON.parse(text) : {}; - break; - } - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - throw jsonError ?? textError; - } catch (error) { - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); - } - } - - finalError = finalError || {}; - - if (throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return responseStyle === 'data' - ? undefined - : { - error: finalError, - request, - response, - }; - } - }; - - const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); - - const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, - url, - }); - }; - - const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); - - return { - buildUrl: _buildUrl, - connect: makeMethodFn('CONNECT'), - delete: makeMethodFn('DELETE'), - get: makeMethodFn('GET'), - getConfig, - head: makeMethodFn('HEAD'), - interceptors, - options: makeMethodFn('OPTIONS'), - patch: makeMethodFn('PATCH'), - post: makeMethodFn('POST'), - put: makeMethodFn('PUT'), - request, - setConfig, - sse: { - connect: makeSseFn('CONNECT'), - delete: makeSseFn('DELETE'), - get: makeSseFn('GET'), - head: makeSseFn('HEAD'), - options: makeSseFn('OPTIONS'), - patch: makeSseFn('PATCH'), - post: makeSseFn('POST'), - put: makeSseFn('PUT'), - trace: makeSseFn('TRACE'), - }, - trace: makeMethodFn('TRACE'), - } as Client; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/index.ts deleted file mode 100644 index b295edeca0..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/types.gen.ts deleted file mode 100644 index 4b288a5099..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/types.gen.ts +++ /dev/null @@ -1,217 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from '../core/serverSentEvents.gen'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> - extends - Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - | 'onRequest' - | 'onSseError' - | 'onSseEvent' - | 'sseDefaultRetryDelay' - | 'sseMaxRetryAttempts' - | 'sseMaxRetryDelay' - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - headers: Headers; - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? (TData extends Record ? TData[keyof TData] : TData) | undefined - : ( - | { - data: TData extends Record ? TData[keyof TData] : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record ? TError[keyof TError] : TError; - } - ) & { - /** request may be undefined, because error may be from building the request object itself */ - request?: Request; - /** response may be undefined, because error may be from building the request object itself or from a network error */ - response?: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => Promise>; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick>, 'method'>, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: TData & Options, -) => string; - -export type Client = CoreClient & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts deleted file mode 100644 index 7800fe4b9d..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/client/utils.gen.ts +++ /dev/null @@ -1,316 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import { getUrl } from '../core/utils.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -export const createQuerySerializer = ({ - parameters = {}, - ...args -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - const options = parameters[name] || args; - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'form', - value, - ...options.array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...options.object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = (contentType: string | null): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export async function setAuthParams( - options: Pick & { - headers: Headers; - }, -): Promise { - for (const auth of options.security ?? []) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -} - -export const buildUrl: Client['buildUrl'] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } - - const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e., their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - /** response may be undefined due to a network error where no response object is produced */ - response: Res | undefined, - /** request may be undefined, because error may be from building the request object itself */ - request: Req | undefined, - options: Options, -) => Err | Promise; - -type ReqInterceptor = (request: Req, options: Options) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - fns: Array = []; - - clear(): void { - this.fns = []; - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } - - update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } - - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } -} - -export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; -} - -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/auth.gen.ts deleted file mode 100644 index 3ebf994788..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/auth.gen.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/bodySerializer.gen.ts deleted file mode 100644 index 67daca60f8..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/bodySerializer.gen.ts +++ /dev/null @@ -1,82 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: unknown) => unknown; - -type QuerySerializerOptionsObject = { - allowReserved?: boolean; - array?: Partial>; - object?: Partial>; -}; - -export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record; -}; - -const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: (body: unknown): FormData => { - const data = new FormData(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: unknown): string => - JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: (body: unknown): string => { - const data = new URLSearchParams(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/params.gen.ts deleted file mode 100644 index 7955601a5c..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/params.gen.ts +++ /dev/null @@ -1,169 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - | { - in: Slot; - map?: string; - } - | { - in?: never; - map: Slot; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if ('key' in config) { - map.set(config.key, { - map: config.map, - }); - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - if (field.in) { - (params[field.in] as Record)[name] = arg; - } - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - if (field.in) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - params[field.map] = value; - } - } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[key.slice(prefix.length)] = value; - } else if ('allowExtra' in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/pathSerializer.gen.ts deleted file mode 100644 index 994b2848c6..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/pathSerializer.gen.ts +++ /dev/null @@ -1,171 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts deleted file mode 100644 index 5000df606f..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,117 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; -}; - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } -}; - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== 'object') { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; -}; - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); - const result: Record = {}; - - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } - - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } - - return result; -}; - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { - if (value === null) { - return null; - } - - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return value; - } - - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - - if (typeof value === 'bigint') { - return value.toString(); - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } - - if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { - return serializeSearchParams(value); - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } - - return undefined; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts deleted file mode 100644 index ddf3c4d13a..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,242 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from './types.gen'; - -export type ServerSentEventsOptions = Omit & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit['body']; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; - -export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; -} - -export type ServerSentEventsResult = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; -}; - -export function createSseClient({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult { - let lastEventId: string | undefined; - - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; - - while (true) { - if (signal.aborted) break; - - attempt++; - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); - - if (lastEventId !== undefined) { - headers.set('Last-Event-ID', lastEventId); - } - - try { - const requestInit: RequestInit = { - redirect: 'follow', - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); - - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); - - if (!response.body) throw new Error('No body in SSE response'); - - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - - let buffer = ''; - - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; - - signal.addEventListener('abort', abortHandler); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings - - const chunks = buffer.split('\n\n'); - buffer = chunks.pop() ?? ''; - - for (const chunk of chunks) { - const lines = chunk.split('\n'); - const dataLines: Array = []; - let eventName: string | undefined; - - for (const line of lines) { - if (line.startsWith('data:')) { - dataLines.push(line.replace(/^data:\s*/, '')); - } else if (line.startsWith('event:')) { - eventName = line.replace(/^event:\s*/, ''); - } else if (line.startsWith('id:')) { - lastEventId = line.replace(/^id:\s*/, ''); - } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } - - let data: unknown; - let parsedJson = false; - - if (dataLines.length) { - const rawData = dataLines.join('\n'); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } - - if (responseTransformer) { - data = await responseTransformer(data); - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); - - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener('abort', abortHandler); - reader.releaseLock(); - } - - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); - - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { - break; // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); - await sleep(backoff); - } - } - }; - - const stream = createStream(); - - return { stream }; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/types.gen.ts deleted file mode 100644 index 9efe71d4c1..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/types.gen.ts +++ /dev/null @@ -1,104 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; - -export type HttpMethod = - | 'connect' - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'trace'; - -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; -} & { - [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - string | number | boolean | (string | number | boolean)[] | null | undefined | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g., converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/utils.gen.ts deleted file mode 100644 index 9a4fec7830..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/core/utils.gen.ts +++ /dev/null @@ -1,140 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from './pathSerializer.gen'; - -export interface PathSerializer { - path: Record; - url: string; -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g; - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; -}) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; - - if (isSerializedBody) { - if ('serializedBody' in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ''; - - return hasSerializedBody ? options.serializedBody : null; - } - - // not all clients implement a serializedBody property (i.e., client-axios) - return options.body !== '' ? options.body : null; - } - - // plain/text body - if (hasBody) { - return options.body; - } - - // no body was provided - return undefined; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/index.ts deleted file mode 100644 index 8dc6d04993..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export { type Options, postFoo } from './sdk.gen'; -export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/sdk.gen.ts deleted file mode 100644 index f741139140..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/sdk.gen.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import * as z from 'zod/mini'; - -import type { Client, Options as Options2, TDataShape } from './client'; -import { client } from './client.gen'; -import type { PostFooData, PostFooResponses } from './types.gen'; -import { zPostFooResponse } from './zod.gen'; - -export type Options = Options2 & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; -}; - -export const postFoo = (options?: Options) => (options?.client ?? client).post({ - requestValidator: async (data) => await z.object({ - body: z.optional(z.never()), - path: z.optional(z.never()), - query: z.optional(z.never()) - }).parseAsync(data), - responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), - responseValidator: async (data) => await zPostFooResponse.parseAsync(data), - url: '/foo', - ...options -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/zod.gen.ts deleted file mode 100644 index 4820089703..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/zod.gen.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import * as z from 'zod/mini'; - -export const zFoo = z.object({ - bar: z.optional(z.int()), - foo: z._default(z.coerce.bigint().check(z.minimum(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }), z.maximum(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' })), BigInt(0)), - id: z.string() -}); - -export const zBar = z.object({ - foo: z.int() -}); - -/** - * OK - */ -export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client.gen.ts deleted file mode 100644 index cab3c70195..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client.gen.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from './client'; -import type { ClientOptions as ClientOptions2 } from './types.gen'; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = (override?: Config) => Config & T>; - -export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts deleted file mode 100644 index fc3f037f16..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/client.gen.ts +++ /dev/null @@ -1,277 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from '../core/serverSentEvents.gen'; -import type { HttpMethod } from '../core/types.gen'; -import { getValidRequestBody } from '../core/utils.gen'; -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors(); - - const beforeRequest = async < - TData = unknown, - TResponseStyle extends 'data' | 'fields' = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, - >( - options: RequestOptions, - ) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined as string | undefined, - }; - - if (opts.security) { - await setAuthParams(opts); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const resolvedOpts = opts as typeof opts & - ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - - return { opts: resolvedOpts, url }; - }; - - const request: Client['request'] = async (options) => { - const throwOnError = options.throwOnError ?? _config.throwOnError; - const responseStyle = options.responseStyle ?? _config.responseStyle; - - let request: Request | undefined; - let response: Response | undefined; - - try { - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: getValidRequestBody(opts), - }; - - request = new Request(url, requestInit); - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - - response = await _fetch(request); - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (response.status === 204 || response.headers.get('Content-Length') === '0') { - let emptyData: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'text': - emptyData = await response[parseAs](); - break; - case 'formData': - emptyData = new FormData(); - break; - case 'stream': - emptyData = response.body; - break; - case 'json': - default: - emptyData = {}; - break; - } - return opts.responseStyle === 'data' - ? emptyData - : { - data: emptyData, - ...result, - }; - } - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'text': - data = await response[parseAs](); - break; - case 'json': { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text(); - data = text ? JSON.parse(text) : {}; - break; - } - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - throw jsonError ?? textError; - } catch (error) { - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); - } - } - - finalError = finalError || {}; - - if (throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return responseStyle === 'data' - ? undefined - : { - error: finalError, - request, - response, - }; - } - }; - - const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); - - const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, - url, - }); - }; - - const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); - - return { - buildUrl: _buildUrl, - connect: makeMethodFn('CONNECT'), - delete: makeMethodFn('DELETE'), - get: makeMethodFn('GET'), - getConfig, - head: makeMethodFn('HEAD'), - interceptors, - options: makeMethodFn('OPTIONS'), - patch: makeMethodFn('PATCH'), - post: makeMethodFn('POST'), - put: makeMethodFn('PUT'), - request, - setConfig, - sse: { - connect: makeSseFn('CONNECT'), - delete: makeSseFn('DELETE'), - get: makeSseFn('GET'), - head: makeSseFn('HEAD'), - options: makeSseFn('OPTIONS'), - patch: makeSseFn('PATCH'), - post: makeSseFn('POST'), - put: makeSseFn('PUT'), - trace: makeSseFn('TRACE'), - }, - trace: makeMethodFn('TRACE'), - } as Client; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/index.ts deleted file mode 100644 index b295edeca0..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/types.gen.ts deleted file mode 100644 index 4b288a5099..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/types.gen.ts +++ /dev/null @@ -1,217 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from '../core/serverSentEvents.gen'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> - extends - Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - | 'onRequest' - | 'onSseError' - | 'onSseEvent' - | 'sseDefaultRetryDelay' - | 'sseMaxRetryAttempts' - | 'sseMaxRetryDelay' - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - headers: Headers; - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? (TData extends Record ? TData[keyof TData] : TData) | undefined - : ( - | { - data: TData extends Record ? TData[keyof TData] : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record ? TError[keyof TError] : TError; - } - ) & { - /** request may be undefined, because error may be from building the request object itself */ - request?: Request; - /** response may be undefined, because error may be from building the request object itself or from a network error */ - response?: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => Promise>; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick>, 'method'>, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: TData & Options, -) => string; - -export type Client = CoreClient & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts deleted file mode 100644 index 7800fe4b9d..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/client/utils.gen.ts +++ /dev/null @@ -1,316 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import { getUrl } from '../core/utils.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -export const createQuerySerializer = ({ - parameters = {}, - ...args -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - const options = parameters[name] || args; - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'form', - value, - ...options.array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...options.object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = (contentType: string | null): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export async function setAuthParams( - options: Pick & { - headers: Headers; - }, -): Promise { - for (const auth of options.security ?? []) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -} - -export const buildUrl: Client['buildUrl'] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } - - const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e., their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - /** response may be undefined due to a network error where no response object is produced */ - response: Res | undefined, - /** request may be undefined, because error may be from building the request object itself */ - request: Req | undefined, - options: Options, -) => Err | Promise; - -type ReqInterceptor = (request: Req, options: Options) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - fns: Array = []; - - clear(): void { - this.fns = []; - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } - - update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } - - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } -} - -export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; -} - -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/auth.gen.ts deleted file mode 100644 index 3ebf994788..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/auth.gen.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/bodySerializer.gen.ts deleted file mode 100644 index 67daca60f8..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/bodySerializer.gen.ts +++ /dev/null @@ -1,82 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: unknown) => unknown; - -type QuerySerializerOptionsObject = { - allowReserved?: boolean; - array?: Partial>; - object?: Partial>; -}; - -export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record; -}; - -const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: (body: unknown): FormData => { - const data = new FormData(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: unknown): string => - JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: (body: unknown): string => { - const data = new URLSearchParams(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/params.gen.ts deleted file mode 100644 index 7955601a5c..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/params.gen.ts +++ /dev/null @@ -1,169 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - | { - in: Slot; - map?: string; - } - | { - in?: never; - map: Slot; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if ('key' in config) { - map.set(config.key, { - map: config.map, - }); - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - if (field.in) { - (params[field.in] as Record)[name] = arg; - } - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - if (field.in) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - params[field.map] = value; - } - } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[key.slice(prefix.length)] = value; - } else if ('allowExtra' in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/pathSerializer.gen.ts deleted file mode 100644 index 994b2848c6..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/pathSerializer.gen.ts +++ /dev/null @@ -1,171 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts deleted file mode 100644 index 5000df606f..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,117 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; -}; - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } -}; - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== 'object') { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; -}; - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); - const result: Record = {}; - - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } - - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } - - return result; -}; - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { - if (value === null) { - return null; - } - - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return value; - } - - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - - if (typeof value === 'bigint') { - return value.toString(); - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } - - if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { - return serializeSearchParams(value); - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } - - return undefined; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts deleted file mode 100644 index ddf3c4d13a..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,242 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from './types.gen'; - -export type ServerSentEventsOptions = Omit & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit['body']; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; - -export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; -} - -export type ServerSentEventsResult = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; -}; - -export function createSseClient({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult { - let lastEventId: string | undefined; - - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; - - while (true) { - if (signal.aborted) break; - - attempt++; - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); - - if (lastEventId !== undefined) { - headers.set('Last-Event-ID', lastEventId); - } - - try { - const requestInit: RequestInit = { - redirect: 'follow', - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); - - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); - - if (!response.body) throw new Error('No body in SSE response'); - - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - - let buffer = ''; - - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; - - signal.addEventListener('abort', abortHandler); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings - - const chunks = buffer.split('\n\n'); - buffer = chunks.pop() ?? ''; - - for (const chunk of chunks) { - const lines = chunk.split('\n'); - const dataLines: Array = []; - let eventName: string | undefined; - - for (const line of lines) { - if (line.startsWith('data:')) { - dataLines.push(line.replace(/^data:\s*/, '')); - } else if (line.startsWith('event:')) { - eventName = line.replace(/^event:\s*/, ''); - } else if (line.startsWith('id:')) { - lastEventId = line.replace(/^id:\s*/, ''); - } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } - - let data: unknown; - let parsedJson = false; - - if (dataLines.length) { - const rawData = dataLines.join('\n'); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } - - if (responseTransformer) { - data = await responseTransformer(data); - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); - - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener('abort', abortHandler); - reader.releaseLock(); - } - - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); - - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { - break; // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); - await sleep(backoff); - } - } - }; - - const stream = createStream(); - - return { stream }; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/types.gen.ts deleted file mode 100644 index 9efe71d4c1..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/types.gen.ts +++ /dev/null @@ -1,104 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; - -export type HttpMethod = - | 'connect' - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'trace'; - -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; -} & { - [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - string | number | boolean | (string | number | boolean)[] | null | undefined | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g., converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/utils.gen.ts deleted file mode 100644 index 9a4fec7830..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/core/utils.gen.ts +++ /dev/null @@ -1,140 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from './pathSerializer.gen'; - -export interface PathSerializer { - path: Record; - url: string; -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g; - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; -}) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; - - if (isSerializedBody) { - if ('serializedBody' in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ''; - - return hasSerializedBody ? options.serializedBody : null; - } - - // not all clients implement a serializedBody property (i.e., client-axios) - return options.body !== '' ? options.body : null; - } - - // plain/text body - if (hasBody) { - return options.body; - } - - // no body was provided - return undefined; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/index.ts deleted file mode 100644 index 8dc6d04993..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export { type Options, postFoo } from './sdk.gen'; -export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/sdk.gen.ts deleted file mode 100644 index f95fb60307..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/sdk.gen.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { z } from 'zod/v3'; - -import type { Client, Options as Options2, TDataShape } from './client'; -import { client } from './client.gen'; -import type { PostFooData, PostFooResponses } from './types.gen'; -import { zPostFooResponse } from './zod.gen'; - -export type Options = Options2 & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; -}; - -export const postFoo = (options?: Options) => (options?.client ?? client).post({ - requestValidator: async (data) => await z.object({ - body: z.never().optional(), - path: z.never().optional(), - query: z.never().optional() - }).parseAsync(data), - responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), - responseValidator: async (data) => await zPostFooResponse.parseAsync(data), - url: '/foo', - ...options -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/zod.gen.ts deleted file mode 100644 index 01c7ccbc5b..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/zod.gen.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { z } from 'zod/v3'; - -export const zFoo = z.object({ - bar: z.number().int().optional(), - foo: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).default(BigInt(0)), - id: z.string() -}); - -export const zBar = z.object({ - foo: z.number().int() -}); - -/** - * OK - */ -export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client.gen.ts deleted file mode 100644 index cab3c70195..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client.gen.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from './client'; -import type { ClientOptions as ClientOptions2 } from './types.gen'; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = (override?: Config) => Config & T>; - -export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts deleted file mode 100644 index fc3f037f16..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/client.gen.ts +++ /dev/null @@ -1,277 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from '../core/serverSentEvents.gen'; -import type { HttpMethod } from '../core/types.gen'; -import { getValidRequestBody } from '../core/utils.gen'; -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors(); - - const beforeRequest = async < - TData = unknown, - TResponseStyle extends 'data' | 'fields' = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, - >( - options: RequestOptions, - ) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined as string | undefined, - }; - - if (opts.security) { - await setAuthParams(opts); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const resolvedOpts = opts as typeof opts & - ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - - return { opts: resolvedOpts, url }; - }; - - const request: Client['request'] = async (options) => { - const throwOnError = options.throwOnError ?? _config.throwOnError; - const responseStyle = options.responseStyle ?? _config.responseStyle; - - let request: Request | undefined; - let response: Response | undefined; - - try { - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: getValidRequestBody(opts), - }; - - request = new Request(url, requestInit); - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - - response = await _fetch(request); - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (response.status === 204 || response.headers.get('Content-Length') === '0') { - let emptyData: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'text': - emptyData = await response[parseAs](); - break; - case 'formData': - emptyData = new FormData(); - break; - case 'stream': - emptyData = response.body; - break; - case 'json': - default: - emptyData = {}; - break; - } - return opts.responseStyle === 'data' - ? emptyData - : { - data: emptyData, - ...result, - }; - } - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'text': - data = await response[parseAs](); - break; - case 'json': { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text(); - data = text ? JSON.parse(text) : {}; - break; - } - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - throw jsonError ?? textError; - } catch (error) { - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); - } - } - - finalError = finalError || {}; - - if (throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return responseStyle === 'data' - ? undefined - : { - error: finalError, - request, - response, - }; - } - }; - - const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); - - const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, - url, - }); - }; - - const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); - - return { - buildUrl: _buildUrl, - connect: makeMethodFn('CONNECT'), - delete: makeMethodFn('DELETE'), - get: makeMethodFn('GET'), - getConfig, - head: makeMethodFn('HEAD'), - interceptors, - options: makeMethodFn('OPTIONS'), - patch: makeMethodFn('PATCH'), - post: makeMethodFn('POST'), - put: makeMethodFn('PUT'), - request, - setConfig, - sse: { - connect: makeSseFn('CONNECT'), - delete: makeSseFn('DELETE'), - get: makeSseFn('GET'), - head: makeSseFn('HEAD'), - options: makeSseFn('OPTIONS'), - patch: makeSseFn('PATCH'), - post: makeSseFn('POST'), - put: makeSseFn('PUT'), - trace: makeSseFn('TRACE'), - }, - trace: makeMethodFn('TRACE'), - } as Client; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/index.ts deleted file mode 100644 index b295edeca0..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/types.gen.ts deleted file mode 100644 index 4b288a5099..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/types.gen.ts +++ /dev/null @@ -1,217 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from '../core/serverSentEvents.gen'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> - extends - Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - | 'onRequest' - | 'onSseError' - | 'onSseEvent' - | 'sseDefaultRetryDelay' - | 'sseMaxRetryAttempts' - | 'sseMaxRetryDelay' - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - headers: Headers; - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? (TData extends Record ? TData[keyof TData] : TData) | undefined - : ( - | { - data: TData extends Record ? TData[keyof TData] : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record ? TError[keyof TError] : TError; - } - ) & { - /** request may be undefined, because error may be from building the request object itself */ - request?: Request; - /** response may be undefined, because error may be from building the request object itself or from a network error */ - response?: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => Promise>; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick>, 'method'>, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: TData & Options, -) => string; - -export type Client = CoreClient & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts deleted file mode 100644 index 7800fe4b9d..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/client/utils.gen.ts +++ /dev/null @@ -1,316 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import { getUrl } from '../core/utils.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -export const createQuerySerializer = ({ - parameters = {}, - ...args -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - const options = parameters[name] || args; - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'form', - value, - ...options.array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...options.object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = (contentType: string | null): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export async function setAuthParams( - options: Pick & { - headers: Headers; - }, -): Promise { - for (const auth of options.security ?? []) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -} - -export const buildUrl: Client['buildUrl'] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } - - const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e., their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - /** response may be undefined due to a network error where no response object is produced */ - response: Res | undefined, - /** request may be undefined, because error may be from building the request object itself */ - request: Req | undefined, - options: Options, -) => Err | Promise; - -type ReqInterceptor = (request: Req, options: Options) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - fns: Array = []; - - clear(): void { - this.fns = []; - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } - - update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } - - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } -} - -export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; -} - -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/auth.gen.ts deleted file mode 100644 index 3ebf994788..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/auth.gen.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/bodySerializer.gen.ts deleted file mode 100644 index 67daca60f8..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/bodySerializer.gen.ts +++ /dev/null @@ -1,82 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: unknown) => unknown; - -type QuerySerializerOptionsObject = { - allowReserved?: boolean; - array?: Partial>; - object?: Partial>; -}; - -export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record; -}; - -const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: (body: unknown): FormData => { - const data = new FormData(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: unknown): string => - JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: (body: unknown): string => { - const data = new URLSearchParams(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/params.gen.ts deleted file mode 100644 index 7955601a5c..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/params.gen.ts +++ /dev/null @@ -1,169 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - | { - in: Slot; - map?: string; - } - | { - in?: never; - map: Slot; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if ('key' in config) { - map.set(config.key, { - map: config.map, - }); - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - if (field.in) { - (params[field.in] as Record)[name] = arg; - } - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - if (field.in) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - params[field.map] = value; - } - } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[key.slice(prefix.length)] = value; - } else if ('allowExtra' in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/pathSerializer.gen.ts deleted file mode 100644 index 994b2848c6..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/pathSerializer.gen.ts +++ /dev/null @@ -1,171 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts deleted file mode 100644 index 5000df606f..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,117 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; -}; - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } -}; - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== 'object') { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; -}; - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); - const result: Record = {}; - - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } - - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } - - return result; -}; - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { - if (value === null) { - return null; - } - - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return value; - } - - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - - if (typeof value === 'bigint') { - return value.toString(); - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } - - if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { - return serializeSearchParams(value); - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } - - return undefined; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts deleted file mode 100644 index ddf3c4d13a..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,242 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from './types.gen'; - -export type ServerSentEventsOptions = Omit & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit['body']; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; - -export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; -} - -export type ServerSentEventsResult = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; -}; - -export function createSseClient({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult { - let lastEventId: string | undefined; - - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; - - while (true) { - if (signal.aborted) break; - - attempt++; - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); - - if (lastEventId !== undefined) { - headers.set('Last-Event-ID', lastEventId); - } - - try { - const requestInit: RequestInit = { - redirect: 'follow', - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); - - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); - - if (!response.body) throw new Error('No body in SSE response'); - - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - - let buffer = ''; - - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; - - signal.addEventListener('abort', abortHandler); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings - - const chunks = buffer.split('\n\n'); - buffer = chunks.pop() ?? ''; - - for (const chunk of chunks) { - const lines = chunk.split('\n'); - const dataLines: Array = []; - let eventName: string | undefined; - - for (const line of lines) { - if (line.startsWith('data:')) { - dataLines.push(line.replace(/^data:\s*/, '')); - } else if (line.startsWith('event:')) { - eventName = line.replace(/^event:\s*/, ''); - } else if (line.startsWith('id:')) { - lastEventId = line.replace(/^id:\s*/, ''); - } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } - - let data: unknown; - let parsedJson = false; - - if (dataLines.length) { - const rawData = dataLines.join('\n'); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } - - if (responseTransformer) { - data = await responseTransformer(data); - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); - - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener('abort', abortHandler); - reader.releaseLock(); - } - - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); - - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { - break; // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); - await sleep(backoff); - } - } - }; - - const stream = createStream(); - - return { stream }; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/types.gen.ts deleted file mode 100644 index 9efe71d4c1..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/types.gen.ts +++ /dev/null @@ -1,104 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; - -export type HttpMethod = - | 'connect' - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'trace'; - -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; -} & { - [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - string | number | boolean | (string | number | boolean)[] | null | undefined | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g., converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/utils.gen.ts deleted file mode 100644 index 9a4fec7830..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/core/utils.gen.ts +++ /dev/null @@ -1,140 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from './pathSerializer.gen'; - -export interface PathSerializer { - path: Record; - url: string; -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g; - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; -}) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; - - if (isSerializedBody) { - if ('serializedBody' in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ''; - - return hasSerializedBody ? options.serializedBody : null; - } - - // not all clients implement a serializedBody property (i.e., client-axios) - return options.body !== '' ? options.body : null; - } - - // plain/text body - if (hasBody) { - return options.body; - } - - // no body was provided - return undefined; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/index.ts deleted file mode 100644 index 8dc6d04993..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export { type Options, postFoo } from './sdk.gen'; -export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/sdk.gen.ts deleted file mode 100644 index 2a0fe9d412..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/sdk.gen.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import * as z from 'zod'; - -import type { Client, Options as Options2, TDataShape } from './client'; -import { client } from './client.gen'; -import type { PostFooData, PostFooResponses } from './types.gen'; -import { zPostFooResponse } from './zod.gen'; - -export type Options = Options2 & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; -}; - -export const postFoo = (options?: Options) => (options?.client ?? client).post({ - requestValidator: async (data) => await z.object({ - body: z.never().optional(), - path: z.never().optional(), - query: z.never().optional() - }).parseAsync(data), - responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), - responseValidator: async (data) => await zPostFooResponse.parseAsync(data), - url: '/foo', - ...options -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/zod.gen.ts deleted file mode 100644 index 6173cb8206..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/zod.gen.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import * as z from 'zod'; - -export const zFoo = z.object({ - bar: z.int().optional(), - foo: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).default(BigInt(0)), - id: z.string() -}); - -export const zBar = z.object({ - foo: z.int() -}); - -/** - * OK - */ -export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/client.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/client.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/client.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/index.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/index.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/index.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/types.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/types.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/types.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/utils.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/client/utils.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/utils.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/auth.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/auth.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/auth.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/bodySerializer.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/bodySerializer.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/bodySerializer.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/params.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/params.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/params.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/pathSerializer.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/pathSerializer.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/pathSerializer.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/queryKeySerializer.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/queryKeySerializer.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/queryKeySerializer.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/serverSentEvents.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/serverSentEvents.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/serverSentEvents.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/types.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/types.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/types.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/utils.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/core/utils.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/core/utils.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/index.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/index.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/index.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/sdk.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/sdk.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/sdk.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/types.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/zod-transformer/types.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/types.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/zod.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/zod-transformer/zod.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/zod.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client.gen.ts deleted file mode 100644 index cab3c70195..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client.gen.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from './client'; -import type { ClientOptions as ClientOptions2 } from './types.gen'; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = (override?: Config) => Config & T>; - -export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts deleted file mode 100644 index fc3f037f16..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/client.gen.ts +++ /dev/null @@ -1,277 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from '../core/serverSentEvents.gen'; -import type { HttpMethod } from '../core/types.gen'; -import { getValidRequestBody } from '../core/utils.gen'; -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors(); - - const beforeRequest = async < - TData = unknown, - TResponseStyle extends 'data' | 'fields' = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, - >( - options: RequestOptions, - ) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined as string | undefined, - }; - - if (opts.security) { - await setAuthParams(opts); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const resolvedOpts = opts as typeof opts & - ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - - return { opts: resolvedOpts, url }; - }; - - const request: Client['request'] = async (options) => { - const throwOnError = options.throwOnError ?? _config.throwOnError; - const responseStyle = options.responseStyle ?? _config.responseStyle; - - let request: Request | undefined; - let response: Response | undefined; - - try { - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: getValidRequestBody(opts), - }; - - request = new Request(url, requestInit); - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - - response = await _fetch(request); - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (response.status === 204 || response.headers.get('Content-Length') === '0') { - let emptyData: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'text': - emptyData = await response[parseAs](); - break; - case 'formData': - emptyData = new FormData(); - break; - case 'stream': - emptyData = response.body; - break; - case 'json': - default: - emptyData = {}; - break; - } - return opts.responseStyle === 'data' - ? emptyData - : { - data: emptyData, - ...result, - }; - } - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'text': - data = await response[parseAs](); - break; - case 'json': { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text(); - data = text ? JSON.parse(text) : {}; - break; - } - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - throw jsonError ?? textError; - } catch (error) { - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); - } - } - - finalError = finalError || {}; - - if (throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return responseStyle === 'data' - ? undefined - : { - error: finalError, - request, - response, - }; - } - }; - - const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); - - const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, - url, - }); - }; - - const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); - - return { - buildUrl: _buildUrl, - connect: makeMethodFn('CONNECT'), - delete: makeMethodFn('DELETE'), - get: makeMethodFn('GET'), - getConfig, - head: makeMethodFn('HEAD'), - interceptors, - options: makeMethodFn('OPTIONS'), - patch: makeMethodFn('PATCH'), - post: makeMethodFn('POST'), - put: makeMethodFn('PUT'), - request, - setConfig, - sse: { - connect: makeSseFn('CONNECT'), - delete: makeSseFn('DELETE'), - get: makeSseFn('GET'), - head: makeSseFn('HEAD'), - options: makeSseFn('OPTIONS'), - patch: makeSseFn('PATCH'), - post: makeSseFn('POST'), - put: makeSseFn('PUT'), - trace: makeSseFn('TRACE'), - }, - trace: makeMethodFn('TRACE'), - } as Client; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/index.ts deleted file mode 100644 index b295edeca0..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/types.gen.ts deleted file mode 100644 index 4b288a5099..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/types.gen.ts +++ /dev/null @@ -1,217 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from '../core/serverSentEvents.gen'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> - extends - Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - | 'onRequest' - | 'onSseError' - | 'onSseEvent' - | 'sseDefaultRetryDelay' - | 'sseMaxRetryAttempts' - | 'sseMaxRetryDelay' - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - headers: Headers; - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? (TData extends Record ? TData[keyof TData] : TData) | undefined - : ( - | { - data: TData extends Record ? TData[keyof TData] : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record ? TError[keyof TError] : TError; - } - ) & { - /** request may be undefined, because error may be from building the request object itself */ - request?: Request; - /** response may be undefined, because error may be from building the request object itself or from a network error */ - response?: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => Promise>; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick>, 'method'>, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: TData & Options, -) => string; - -export type Client = CoreClient & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts deleted file mode 100644 index 7800fe4b9d..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/client/utils.gen.ts +++ /dev/null @@ -1,316 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import { getUrl } from '../core/utils.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -export const createQuerySerializer = ({ - parameters = {}, - ...args -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - const options = parameters[name] || args; - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'form', - value, - ...options.array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...options.object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = (contentType: string | null): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export async function setAuthParams( - options: Pick & { - headers: Headers; - }, -): Promise { - for (const auth of options.security ?? []) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -} - -export const buildUrl: Client['buildUrl'] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } - - const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e., their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - /** response may be undefined due to a network error where no response object is produced */ - response: Res | undefined, - /** request may be undefined, because error may be from building the request object itself */ - request: Req | undefined, - options: Options, -) => Err | Promise; - -type ReqInterceptor = (request: Req, options: Options) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - fns: Array = []; - - clear(): void { - this.fns = []; - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } - - update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } - - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } -} - -export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; -} - -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/auth.gen.ts deleted file mode 100644 index 3ebf994788..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/auth.gen.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/bodySerializer.gen.ts deleted file mode 100644 index 67daca60f8..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/bodySerializer.gen.ts +++ /dev/null @@ -1,82 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: unknown) => unknown; - -type QuerySerializerOptionsObject = { - allowReserved?: boolean; - array?: Partial>; - object?: Partial>; -}; - -export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record; -}; - -const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: (body: unknown): FormData => { - const data = new FormData(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: unknown): string => - JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: (body: unknown): string => { - const data = new URLSearchParams(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/params.gen.ts deleted file mode 100644 index 7955601a5c..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/params.gen.ts +++ /dev/null @@ -1,169 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - | { - in: Slot; - map?: string; - } - | { - in?: never; - map: Slot; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if ('key' in config) { - map.set(config.key, { - map: config.map, - }); - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - if (field.in) { - (params[field.in] as Record)[name] = arg; - } - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - if (field.in) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - params[field.map] = value; - } - } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[key.slice(prefix.length)] = value; - } else if ('allowExtra' in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/pathSerializer.gen.ts deleted file mode 100644 index 994b2848c6..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/pathSerializer.gen.ts +++ /dev/null @@ -1,171 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/queryKeySerializer.gen.ts deleted file mode 100644 index 5000df606f..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,117 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; -}; - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } -}; - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== 'object') { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; -}; - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); - const result: Record = {}; - - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } - - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } - - return result; -}; - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { - if (value === null) { - return null; - } - - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return value; - } - - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - - if (typeof value === 'bigint') { - return value.toString(); - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } - - if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { - return serializeSearchParams(value); - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } - - return undefined; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/serverSentEvents.gen.ts deleted file mode 100644 index ddf3c4d13a..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,242 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from './types.gen'; - -export type ServerSentEventsOptions = Omit & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit['body']; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; - -export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; -} - -export type ServerSentEventsResult = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; -}; - -export function createSseClient({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult { - let lastEventId: string | undefined; - - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; - - while (true) { - if (signal.aborted) break; - - attempt++; - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); - - if (lastEventId !== undefined) { - headers.set('Last-Event-ID', lastEventId); - } - - try { - const requestInit: RequestInit = { - redirect: 'follow', - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); - - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); - - if (!response.body) throw new Error('No body in SSE response'); - - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - - let buffer = ''; - - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; - - signal.addEventListener('abort', abortHandler); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings - - const chunks = buffer.split('\n\n'); - buffer = chunks.pop() ?? ''; - - for (const chunk of chunks) { - const lines = chunk.split('\n'); - const dataLines: Array = []; - let eventName: string | undefined; - - for (const line of lines) { - if (line.startsWith('data:')) { - dataLines.push(line.replace(/^data:\s*/, '')); - } else if (line.startsWith('event:')) { - eventName = line.replace(/^event:\s*/, ''); - } else if (line.startsWith('id:')) { - lastEventId = line.replace(/^id:\s*/, ''); - } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } - - let data: unknown; - let parsedJson = false; - - if (dataLines.length) { - const rawData = dataLines.join('\n'); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } - - if (responseTransformer) { - data = await responseTransformer(data); - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); - - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener('abort', abortHandler); - reader.releaseLock(); - } - - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); - - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { - break; // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); - await sleep(backoff); - } - } - }; - - const stream = createStream(); - - return { stream }; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/types.gen.ts deleted file mode 100644 index 9efe71d4c1..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/types.gen.ts +++ /dev/null @@ -1,104 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; - -export type HttpMethod = - | 'connect' - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'trace'; - -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; -} & { - [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - string | number | boolean | (string | number | boolean)[] | null | undefined | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g., converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/utils.gen.ts deleted file mode 100644 index 9a4fec7830..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/core/utils.gen.ts +++ /dev/null @@ -1,140 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from './pathSerializer.gen'; - -export interface PathSerializer { - path: Record; - url: string; -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g; - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; -}) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; - - if (isSerializedBody) { - if ('serializedBody' in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ''; - - return hasSerializedBody ? options.serializedBody : null; - } - - // not all clients implement a serializedBody property (i.e., client-axios) - return options.body !== '' ? options.body : null; - } - - // plain/text body - if (hasBody) { - return options.body; - } - - // no body was provided - return undefined; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/index.ts deleted file mode 100644 index 8dc6d04993..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export { type Options, postFoo } from './sdk.gen'; -export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/sdk.gen.ts deleted file mode 100644 index f741139140..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/sdk.gen.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import * as z from 'zod/mini'; - -import type { Client, Options as Options2, TDataShape } from './client'; -import { client } from './client.gen'; -import type { PostFooData, PostFooResponses } from './types.gen'; -import { zPostFooResponse } from './zod.gen'; - -export type Options = Options2 & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; -}; - -export const postFoo = (options?: Options) => (options?.client ?? client).post({ - requestValidator: async (data) => await z.object({ - body: z.optional(z.never()), - path: z.optional(z.never()), - query: z.optional(z.never()) - }).parseAsync(data), - responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), - responseValidator: async (data) => await zPostFooResponse.parseAsync(data), - url: '/foo', - ...options -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/types.gen.ts deleted file mode 100644 index 318823eb66..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/types.gen.ts +++ /dev/null @@ -1,36 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: `${string}://${string}` | (string & {}); -}; - -export type TypeID = `${T}_${string}`; - -export type UserId = TypeID<'user'>; - -export type Foo = { - bar?: number; - foo: number; - id: UserId; -}; - -export type Bar = { - foo: number; - [key: string]: number; -}; - -export type PostFooData = { - body?: never; - path?: never; - query?: never; - url: '/foo'; -}; - -export type PostFooResponses = { - /** - * OK - */ - 200: Foo; -}; - -export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/zod.gen.ts deleted file mode 100644 index 4820089703..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/zod-transformer/zod.gen.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import * as z from 'zod/mini'; - -export const zFoo = z.object({ - bar: z.optional(z.int()), - foo: z._default(z.coerce.bigint().check(z.minimum(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }), z.maximum(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' })), BigInt(0)), - id: z.string() -}); - -export const zBar = z.object({ - foo: z.int() -}); - -/** - * OK - */ -export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/client.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/client.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/client.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/index.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/index.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/index.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/types.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/types.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/types.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/utils.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/client/utils.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/utils.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/auth.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/auth.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/auth.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/bodySerializer.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/bodySerializer.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/bodySerializer.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/params.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/params.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/params.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/pathSerializer.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/pathSerializer.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/pathSerializer.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/queryKeySerializer.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/queryKeySerializer.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/queryKeySerializer.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/serverSentEvents.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/serverSentEvents.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/serverSentEvents.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/types.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/types.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/types.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/utils.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/core/utils.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/core/utils.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/index.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/index.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/index.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/sdk.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/sdk.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/sdk.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/types.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/zod-transformer/types.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/types.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/zod.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/zod-transformer/zod.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/zod.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client.gen.ts deleted file mode 100644 index cab3c70195..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client.gen.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from './client'; -import type { ClientOptions as ClientOptions2 } from './types.gen'; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = (override?: Config) => Config & T>; - -export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts deleted file mode 100644 index fc3f037f16..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/client.gen.ts +++ /dev/null @@ -1,277 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from '../core/serverSentEvents.gen'; -import type { HttpMethod } from '../core/types.gen'; -import { getValidRequestBody } from '../core/utils.gen'; -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors(); - - const beforeRequest = async < - TData = unknown, - TResponseStyle extends 'data' | 'fields' = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, - >( - options: RequestOptions, - ) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined as string | undefined, - }; - - if (opts.security) { - await setAuthParams(opts); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const resolvedOpts = opts as typeof opts & - ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - - return { opts: resolvedOpts, url }; - }; - - const request: Client['request'] = async (options) => { - const throwOnError = options.throwOnError ?? _config.throwOnError; - const responseStyle = options.responseStyle ?? _config.responseStyle; - - let request: Request | undefined; - let response: Response | undefined; - - try { - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: getValidRequestBody(opts), - }; - - request = new Request(url, requestInit); - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - - response = await _fetch(request); - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (response.status === 204 || response.headers.get('Content-Length') === '0') { - let emptyData: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'text': - emptyData = await response[parseAs](); - break; - case 'formData': - emptyData = new FormData(); - break; - case 'stream': - emptyData = response.body; - break; - case 'json': - default: - emptyData = {}; - break; - } - return opts.responseStyle === 'data' - ? emptyData - : { - data: emptyData, - ...result, - }; - } - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'text': - data = await response[parseAs](); - break; - case 'json': { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text(); - data = text ? JSON.parse(text) : {}; - break; - } - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - throw jsonError ?? textError; - } catch (error) { - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); - } - } - - finalError = finalError || {}; - - if (throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return responseStyle === 'data' - ? undefined - : { - error: finalError, - request, - response, - }; - } - }; - - const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); - - const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, - url, - }); - }; - - const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); - - return { - buildUrl: _buildUrl, - connect: makeMethodFn('CONNECT'), - delete: makeMethodFn('DELETE'), - get: makeMethodFn('GET'), - getConfig, - head: makeMethodFn('HEAD'), - interceptors, - options: makeMethodFn('OPTIONS'), - patch: makeMethodFn('PATCH'), - post: makeMethodFn('POST'), - put: makeMethodFn('PUT'), - request, - setConfig, - sse: { - connect: makeSseFn('CONNECT'), - delete: makeSseFn('DELETE'), - get: makeSseFn('GET'), - head: makeSseFn('HEAD'), - options: makeSseFn('OPTIONS'), - patch: makeSseFn('PATCH'), - post: makeSseFn('POST'), - put: makeSseFn('PUT'), - trace: makeSseFn('TRACE'), - }, - trace: makeMethodFn('TRACE'), - } as Client; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/index.ts deleted file mode 100644 index b295edeca0..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/types.gen.ts deleted file mode 100644 index 4b288a5099..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/types.gen.ts +++ /dev/null @@ -1,217 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from '../core/serverSentEvents.gen'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> - extends - Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - | 'onRequest' - | 'onSseError' - | 'onSseEvent' - | 'sseDefaultRetryDelay' - | 'sseMaxRetryAttempts' - | 'sseMaxRetryDelay' - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - headers: Headers; - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? (TData extends Record ? TData[keyof TData] : TData) | undefined - : ( - | { - data: TData extends Record ? TData[keyof TData] : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record ? TError[keyof TError] : TError; - } - ) & { - /** request may be undefined, because error may be from building the request object itself */ - request?: Request; - /** response may be undefined, because error may be from building the request object itself or from a network error */ - response?: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => Promise>; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick>, 'method'>, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: TData & Options, -) => string; - -export type Client = CoreClient & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts deleted file mode 100644 index 7800fe4b9d..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/client/utils.gen.ts +++ /dev/null @@ -1,316 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import { getUrl } from '../core/utils.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -export const createQuerySerializer = ({ - parameters = {}, - ...args -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - const options = parameters[name] || args; - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'form', - value, - ...options.array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...options.object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = (contentType: string | null): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export async function setAuthParams( - options: Pick & { - headers: Headers; - }, -): Promise { - for (const auth of options.security ?? []) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -} - -export const buildUrl: Client['buildUrl'] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } - - const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e., their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - /** response may be undefined due to a network error where no response object is produced */ - response: Res | undefined, - /** request may be undefined, because error may be from building the request object itself */ - request: Req | undefined, - options: Options, -) => Err | Promise; - -type ReqInterceptor = (request: Req, options: Options) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - fns: Array = []; - - clear(): void { - this.fns = []; - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } - - update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } - - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } -} - -export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; -} - -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/auth.gen.ts deleted file mode 100644 index 3ebf994788..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/auth.gen.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/bodySerializer.gen.ts deleted file mode 100644 index 67daca60f8..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/bodySerializer.gen.ts +++ /dev/null @@ -1,82 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: unknown) => unknown; - -type QuerySerializerOptionsObject = { - allowReserved?: boolean; - array?: Partial>; - object?: Partial>; -}; - -export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record; -}; - -const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: (body: unknown): FormData => { - const data = new FormData(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: unknown): string => - JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: (body: unknown): string => { - const data = new URLSearchParams(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/params.gen.ts deleted file mode 100644 index 7955601a5c..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/params.gen.ts +++ /dev/null @@ -1,169 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - | { - in: Slot; - map?: string; - } - | { - in?: never; - map: Slot; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if ('key' in config) { - map.set(config.key, { - map: config.map, - }); - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - if (field.in) { - (params[field.in] as Record)[name] = arg; - } - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - if (field.in) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - params[field.map] = value; - } - } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[key.slice(prefix.length)] = value; - } else if ('allowExtra' in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/pathSerializer.gen.ts deleted file mode 100644 index 994b2848c6..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/pathSerializer.gen.ts +++ /dev/null @@ -1,171 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/queryKeySerializer.gen.ts deleted file mode 100644 index 5000df606f..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,117 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; -}; - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } -}; - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== 'object') { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; -}; - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); - const result: Record = {}; - - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } - - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } - - return result; -}; - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { - if (value === null) { - return null; - } - - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return value; - } - - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - - if (typeof value === 'bigint') { - return value.toString(); - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } - - if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { - return serializeSearchParams(value); - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } - - return undefined; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/serverSentEvents.gen.ts deleted file mode 100644 index ddf3c4d13a..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,242 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from './types.gen'; - -export type ServerSentEventsOptions = Omit & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit['body']; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; - -export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; -} - -export type ServerSentEventsResult = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; -}; - -export function createSseClient({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult { - let lastEventId: string | undefined; - - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; - - while (true) { - if (signal.aborted) break; - - attempt++; - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); - - if (lastEventId !== undefined) { - headers.set('Last-Event-ID', lastEventId); - } - - try { - const requestInit: RequestInit = { - redirect: 'follow', - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); - - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); - - if (!response.body) throw new Error('No body in SSE response'); - - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - - let buffer = ''; - - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; - - signal.addEventListener('abort', abortHandler); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings - - const chunks = buffer.split('\n\n'); - buffer = chunks.pop() ?? ''; - - for (const chunk of chunks) { - const lines = chunk.split('\n'); - const dataLines: Array = []; - let eventName: string | undefined; - - for (const line of lines) { - if (line.startsWith('data:')) { - dataLines.push(line.replace(/^data:\s*/, '')); - } else if (line.startsWith('event:')) { - eventName = line.replace(/^event:\s*/, ''); - } else if (line.startsWith('id:')) { - lastEventId = line.replace(/^id:\s*/, ''); - } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } - - let data: unknown; - let parsedJson = false; - - if (dataLines.length) { - const rawData = dataLines.join('\n'); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } - - if (responseTransformer) { - data = await responseTransformer(data); - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); - - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener('abort', abortHandler); - reader.releaseLock(); - } - - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); - - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { - break; // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); - await sleep(backoff); - } - } - }; - - const stream = createStream(); - - return { stream }; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/types.gen.ts deleted file mode 100644 index 9efe71d4c1..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/types.gen.ts +++ /dev/null @@ -1,104 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; - -export type HttpMethod = - | 'connect' - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'trace'; - -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; -} & { - [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - string | number | boolean | (string | number | boolean)[] | null | undefined | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g., converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/utils.gen.ts deleted file mode 100644 index 9a4fec7830..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/core/utils.gen.ts +++ /dev/null @@ -1,140 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from './pathSerializer.gen'; - -export interface PathSerializer { - path: Record; - url: string; -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g; - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; -}) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; - - if (isSerializedBody) { - if ('serializedBody' in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ''; - - return hasSerializedBody ? options.serializedBody : null; - } - - // not all clients implement a serializedBody property (i.e., client-axios) - return options.body !== '' ? options.body : null; - } - - // plain/text body - if (hasBody) { - return options.body; - } - - // no body was provided - return undefined; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/index.ts deleted file mode 100644 index 8dc6d04993..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export { type Options, postFoo } from './sdk.gen'; -export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/sdk.gen.ts deleted file mode 100644 index f95fb60307..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/sdk.gen.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { z } from 'zod/v3'; - -import type { Client, Options as Options2, TDataShape } from './client'; -import { client } from './client.gen'; -import type { PostFooData, PostFooResponses } from './types.gen'; -import { zPostFooResponse } from './zod.gen'; - -export type Options = Options2 & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; -}; - -export const postFoo = (options?: Options) => (options?.client ?? client).post({ - requestValidator: async (data) => await z.object({ - body: z.never().optional(), - path: z.never().optional(), - query: z.never().optional() - }).parseAsync(data), - responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), - responseValidator: async (data) => await zPostFooResponse.parseAsync(data), - url: '/foo', - ...options -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/types.gen.ts deleted file mode 100644 index 318823eb66..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/types.gen.ts +++ /dev/null @@ -1,36 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: `${string}://${string}` | (string & {}); -}; - -export type TypeID = `${T}_${string}`; - -export type UserId = TypeID<'user'>; - -export type Foo = { - bar?: number; - foo: number; - id: UserId; -}; - -export type Bar = { - foo: number; - [key: string]: number; -}; - -export type PostFooData = { - body?: never; - path?: never; - query?: never; - url: '/foo'; -}; - -export type PostFooResponses = { - /** - * OK - */ - 200: Foo; -}; - -export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/zod.gen.ts deleted file mode 100644 index 01c7ccbc5b..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/zod-transformer/zod.gen.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { z } from 'zod/v3'; - -export const zFoo = z.object({ - bar: z.number().int().optional(), - foo: z.coerce.bigint().min(BigInt('-9223372036854775808'), { message: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { message: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).default(BigInt(0)), - id: z.string() -}); - -export const zBar = z.object({ - foo: z.number().int() -}); - -/** - * OK - */ -export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/client.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/client.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/client.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/index.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/index.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/index.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/types.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/types.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/types.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/utils.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/client/utils.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/utils.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/auth.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/auth.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/auth.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/bodySerializer.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/bodySerializer.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/bodySerializer.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/params.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/params.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/params.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/pathSerializer.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/pathSerializer.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/pathSerializer.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/queryKeySerializer.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/queryKeySerializer.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/queryKeySerializer.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/serverSentEvents.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/serverSentEvents.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/serverSentEvents.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/types.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/types.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/types.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/utils.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/core/utils.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/core/utils.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/index.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/index.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/index.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/sdk.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/sdk.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/sdk.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/types.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/zod-transformer/types.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/types.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/zod.gen.ts similarity index 100% rename from packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/zod-transformer/zod.gen.ts rename to packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/zod.gen.ts diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client.gen.ts deleted file mode 100644 index cab3c70195..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client.gen.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from './client'; -import type { ClientOptions as ClientOptions2 } from './types.gen'; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = (override?: Config) => Config & T>; - -export const client = createClient(createConfig()); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts deleted file mode 100644 index fc3f037f16..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/client.gen.ts +++ /dev/null @@ -1,277 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from '../core/serverSentEvents.gen'; -import type { HttpMethod } from '../core/types.gen'; -import { getValidRequestBody } from '../core/utils.gen'; -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors(); - - const beforeRequest = async < - TData = unknown, - TResponseStyle extends 'data' | 'fields' = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, - >( - options: RequestOptions, - ) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined as string | undefined, - }; - - if (opts.security) { - await setAuthParams(opts); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const resolvedOpts = opts as typeof opts & - ResolvedRequestOptions; - const url = buildUrl(resolvedOpts); - - return { opts: resolvedOpts, url }; - }; - - const request: Client['request'] = async (options) => { - const throwOnError = options.throwOnError ?? _config.throwOnError; - const responseStyle = options.responseStyle ?? _config.responseStyle; - - let request: Request | undefined; - let response: Response | undefined; - - try { - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: getValidRequestBody(opts), - }; - - request = new Request(url, requestInit); - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - - response = await _fetch(request); - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (response.status === 204 || response.headers.get('Content-Length') === '0') { - let emptyData: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'text': - emptyData = await response[parseAs](); - break; - case 'formData': - emptyData = new FormData(); - break; - case 'stream': - emptyData = response.body; - break; - case 'json': - default: - emptyData = {}; - break; - } - return opts.responseStyle === 'data' - ? emptyData - : { - data: emptyData, - ...result, - }; - } - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'text': - data = await response[parseAs](); - break; - case 'json': { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text(); - data = text ? JSON.parse(text) : {}; - break; - } - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - throw jsonError ?? textError; - } catch (error) { - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); - } - } - - finalError = finalError || {}; - - if (throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return responseStyle === 'data' - ? undefined - : { - error: finalError, - request, - response, - }; - } - }; - - const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); - - const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, - url, - }); - }; - - const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); - - return { - buildUrl: _buildUrl, - connect: makeMethodFn('CONNECT'), - delete: makeMethodFn('DELETE'), - get: makeMethodFn('GET'), - getConfig, - head: makeMethodFn('HEAD'), - interceptors, - options: makeMethodFn('OPTIONS'), - patch: makeMethodFn('PATCH'), - post: makeMethodFn('POST'), - put: makeMethodFn('PUT'), - request, - setConfig, - sse: { - connect: makeSseFn('CONNECT'), - delete: makeSseFn('DELETE'), - get: makeSseFn('GET'), - head: makeSseFn('HEAD'), - options: makeSseFn('OPTIONS'), - patch: makeSseFn('PATCH'), - post: makeSseFn('POST'), - put: makeSseFn('PUT'), - trace: makeSseFn('TRACE'), - }, - trace: makeMethodFn('TRACE'), - } as Client; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/index.ts deleted file mode 100644 index b295edeca0..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/types.gen.ts deleted file mode 100644 index 4b288a5099..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/types.gen.ts +++ /dev/null @@ -1,217 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from '../core/serverSentEvents.gen'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> - extends - Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - | 'onRequest' - | 'onSseError' - | 'onSseEvent' - | 'sseDefaultRetryDelay' - | 'sseMaxRetryAttempts' - | 'sseMaxRetryDelay' - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - headers: Headers; - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? (TData extends Record ? TData[keyof TData] : TData) | undefined - : ( - | { - data: TData extends Record ? TData[keyof TData] : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record ? TError[keyof TError] : TError; - } - ) & { - /** request may be undefined, because error may be from building the request object itself */ - request?: Request; - /** response may be undefined, because error may be from building the request object itself or from a network error */ - response?: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => Promise>; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick>, 'method'>, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: TData & Options, -) => string; - -export type Client = CoreClient & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - ([TData] extends [never] ? unknown : Omit); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts deleted file mode 100644 index 7800fe4b9d..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/client/utils.gen.ts +++ /dev/null @@ -1,316 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import { getUrl } from '../core/utils.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -export const createQuerySerializer = ({ - parameters = {}, - ...args -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - const options = parameters[name] || args; - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'form', - value, - ...options.array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...options.object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = (contentType: string | null): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export async function setAuthParams( - options: Pick & { - headers: Headers; - }, -): Promise { - for (const auth of options.security ?? []) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -} - -export const buildUrl: Client['buildUrl'] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } - - const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e., their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - /** response may be undefined due to a network error where no response object is produced */ - response: Res | undefined, - /** request may be undefined, because error may be from building the request object itself */ - request: Req | undefined, - options: Options, -) => Err | Promise; - -type ReqInterceptor = (request: Req, options: Options) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - fns: Array = []; - - clear(): void { - this.fns = []; - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } - - update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } - - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } -} - -export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; -} - -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/auth.gen.ts deleted file mode 100644 index 3ebf994788..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/auth.gen.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/bodySerializer.gen.ts deleted file mode 100644 index 67daca60f8..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/bodySerializer.gen.ts +++ /dev/null @@ -1,82 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: unknown) => unknown; - -type QuerySerializerOptionsObject = { - allowReserved?: boolean; - array?: Partial>; - object?: Partial>; -}; - -export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record; -}; - -const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: (body: unknown): FormData => { - const data = new FormData(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: unknown): string => - JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: (body: unknown): string => { - const data = new URLSearchParams(); - - Object.entries(body as Record).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/params.gen.ts deleted file mode 100644 index 7955601a5c..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/params.gen.ts +++ /dev/null @@ -1,169 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - | { - in: Slot; - map?: string; - } - | { - in?: never; - map: Slot; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if ('key' in config) { - map.set(config.key, { - map: config.map, - }); - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - if (field.in) { - (params[field.in] as Record)[name] = arg; - } - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - if (field.in) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - params[field.map] = value; - } - } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[key.slice(prefix.length)] = value; - } else if ('allowExtra' in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/pathSerializer.gen.ts deleted file mode 100644 index 994b2848c6..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/pathSerializer.gen.ts +++ /dev/null @@ -1,171 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/queryKeySerializer.gen.ts deleted file mode 100644 index 5000df606f..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,117 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; -}; - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } -}; - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== 'object') { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; -}; - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); - const result: Record = {}; - - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } - - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } - - return result; -}; - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { - if (value === null) { - return null; - } - - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return value; - } - - if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { - return undefined; - } - - if (typeof value === 'bigint') { - return value.toString(); - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } - - if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { - return serializeSearchParams(value); - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } - - return undefined; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/serverSentEvents.gen.ts deleted file mode 100644 index ddf3c4d13a..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,242 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from './types.gen'; - -export type ServerSentEventsOptions = Omit & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit['body']; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; - -export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; -} - -export type ServerSentEventsResult = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; -}; - -export function createSseClient({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult { - let lastEventId: string | undefined; - - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; - - while (true) { - if (signal.aborted) break; - - attempt++; - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); - - if (lastEventId !== undefined) { - headers.set('Last-Event-ID', lastEventId); - } - - try { - const requestInit: RequestInit = { - redirect: 'follow', - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); - - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); - - if (!response.body) throw new Error('No body in SSE response'); - - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - - let buffer = ''; - - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; - - signal.addEventListener('abort', abortHandler); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings - - const chunks = buffer.split('\n\n'); - buffer = chunks.pop() ?? ''; - - for (const chunk of chunks) { - const lines = chunk.split('\n'); - const dataLines: Array = []; - let eventName: string | undefined; - - for (const line of lines) { - if (line.startsWith('data:')) { - dataLines.push(line.replace(/^data:\s*/, '')); - } else if (line.startsWith('event:')) { - eventName = line.replace(/^event:\s*/, ''); - } else if (line.startsWith('id:')) { - lastEventId = line.replace(/^id:\s*/, ''); - } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } - - let data: unknown; - let parsedJson = false; - - if (dataLines.length) { - const rawData = dataLines.join('\n'); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } - - if (responseTransformer) { - data = await responseTransformer(data); - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); - - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener('abort', abortHandler); - reader.releaseLock(); - } - - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); - - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { - break; // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); - await sleep(backoff); - } - } - }; - - const stream = createStream(); - - return { stream }; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/types.gen.ts deleted file mode 100644 index 9efe71d4c1..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/types.gen.ts +++ /dev/null @@ -1,104 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; - -export type HttpMethod = - | 'connect' - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'trace'; - -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; -} & { - [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - string | number | boolean | (string | number | boolean)[] | null | undefined | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g., converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; -}; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/utils.gen.ts deleted file mode 100644 index 9a4fec7830..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/core/utils.gen.ts +++ /dev/null @@ -1,140 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from './pathSerializer.gen'; - -export interface PathSerializer { - path: Record; - url: string; -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g; - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; -}) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; - - if (isSerializedBody) { - if ('serializedBody' in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ''; - - return hasSerializedBody ? options.serializedBody : null; - } - - // not all clients implement a serializedBody property (i.e., client-axios) - return options.body !== '' ? options.body : null; - } - - // plain/text body - if (hasBody) { - return options.body; - } - - // no body was provided - return undefined; -} diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/index.ts deleted file mode 100644 index 8dc6d04993..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export { type Options, postFoo } from './sdk.gen'; -export type { Bar, ClientOptions, Foo, PostFooData, PostFooResponse, PostFooResponses, TypeID, UserId } from './types.gen'; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/sdk.gen.ts deleted file mode 100644 index 2a0fe9d412..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/sdk.gen.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import * as z from 'zod'; - -import type { Client, Options as Options2, TDataShape } from './client'; -import { client } from './client.gen'; -import type { PostFooData, PostFooResponses } from './types.gen'; -import { zPostFooResponse } from './zod.gen'; - -export type Options = Options2 & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; -}; - -export const postFoo = (options?: Options) => (options?.client ?? client).post({ - requestValidator: async (data) => await z.object({ - body: z.never().optional(), - path: z.never().optional(), - query: z.never().optional() - }).parseAsync(data), - responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), - responseValidator: async (data) => await zPostFooResponse.parseAsync(data), - url: '/foo', - ...options -}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/types.gen.ts deleted file mode 100644 index 318823eb66..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/types.gen.ts +++ /dev/null @@ -1,36 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: `${string}://${string}` | (string & {}); -}; - -export type TypeID = `${T}_${string}`; - -export type UserId = TypeID<'user'>; - -export type Foo = { - bar?: number; - foo: number; - id: UserId; -}; - -export type Bar = { - foo: number; - [key: string]: number; -}; - -export type PostFooData = { - body?: never; - path?: never; - query?: never; - url: '/foo'; -}; - -export type PostFooResponses = { - /** - * OK - */ - 200: Foo; -}; - -export type PostFooResponse = PostFooResponses[keyof PostFooResponses]; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/zod.gen.ts deleted file mode 100644 index 6173cb8206..0000000000 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/zod-transformer/zod.gen.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import * as z from 'zod'; - -export const zFoo = z.object({ - bar: z.int().optional(), - foo: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).default(BigInt(0)), - id: z.string() -}); - -export const zBar = z.object({ - foo: z.int() -}); - -/** - * OK - */ -export const zPostFooResponse = zFoo; diff --git a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts index 1ee06b242b..ab080b99cd 100644 --- a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts @@ -216,6 +216,24 @@ for (const zodVersion of zodVersions) { }), description: 'generates permissive enums with enum resolver', }, + { + config: createConfig({ + input: 'type-format.yaml', + output: 'transformer', + plugins: [ + { + compatibilityVersion: zodVersion.compatibilityVersion, + name: 'zod', + }, + { + name: '@hey-api/sdk', + transformer: 'zod', + validator: true, + }, + ], + }), + description: 'uses zod as response transformer', + }, ]; it.each(scenarios)('$description', async ({ config }) => { diff --git a/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts b/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts index 3b2c12902f..cfc2a9f2ee 100644 --- a/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts @@ -55,25 +55,6 @@ for (const version of versions) { }), description: 'handles various schema types and formats', }, - { - config: createConfig({ - input: 'type-format.yaml', - output: 'zod-transformer', - plugins: [ - '@hey-api/client-fetch', - { - compatibilityVersion: zodVersion.compatibilityVersion, - name: 'zod', - }, - { - name: '@hey-api/sdk', - transformer: 'zod', - validator: true, - }, - ], - }), - description: 'uses zod as response transformer', - }, ]; it.each(scenarios)('$description', async ({ config }) => { From 2fab005312988db455aef833adc4a48f8c6e0465 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 16 May 2026 01:31:50 +0800 Subject: [PATCH 09/14] chore: simplify transformer config --- .../src/plugins/@hey-api/sdk/config.ts | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts index 95e84dac09..5713405c74 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts @@ -7,7 +7,7 @@ import { handler } from './plugin'; import type { HeyApiSdkPlugin } from './types'; const transformerInferWarn = - 'You set `transformer: true` but no transformer plugin was found in your plugins. Add a transformer plugin like `@hey-api/transformers` or a validator plugin like `zod` to enable this feature. The transformer option has been disabled.'; + 'You set `transformer: true` but no transformer plugin was found in your plugins. Add a transformer plugin like `@hey-api/transformers` to enable this feature. The transformer option has been disabled.'; const validatorInferWarn = 'You set `validator: true` but no validator plugin was found in your plugins. Add a validator plugin like `zod` to enable this feature. The validator option has been disabled.'; @@ -19,7 +19,7 @@ export const defaultConfig: HeyApiSdkPlugin['Config'] = { includeInEntry: true, paramsStructure: 'grouped', responseStyle: 'fields', - transformer: { response: false }, + transformer: false, validator: false, // Deprecated - kept for backward compatibility @@ -42,29 +42,20 @@ export const defaultConfig: HeyApiSdkPlugin['Config'] = { plugin.config.client = false; } - // Normalize transformer to object form - if (typeof plugin.config.transformer !== 'object' || plugin.config.transformer === null) { + if (typeof plugin.config.transformer !== 'object') { plugin.config.transformer = { - response: plugin.config.transformer as Exclude, + response: plugin.config.transformer, }; } if (plugin.config.transformer.response) { if (typeof plugin.config.transformer.response === 'boolean') { - // `true`: auto-select transformer plugin, fall back to validator plugin - let resolved: string | undefined | false = false; try { - resolved = context.pluginByTag('transformer'); + plugin.config.transformer.response = context.pluginByTag('transformer'); + plugin.dependencies.add(plugin.config.transformer.response!); } catch { - try { - resolved = context.pluginByTag('validator'); - } catch { - log.warn(transformerInferWarn); - } - } - plugin.config.transformer.response = resolved as typeof plugin.config.transformer.response; - if (resolved) { - plugin.dependencies.add(resolved); + log.warn(transformerInferWarn); + plugin.config.transformer.response = false; } } else { plugin.dependencies.add(plugin.config.transformer.response); From 5af6914a4637aec916bc50305482f8a858b41490 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 16 May 2026 02:07:32 +0800 Subject: [PATCH 10/14] chore: discriminate on function nodes --- .../valibot/v1/test/3.1.x.test.ts | 1 - .../zod/v4/test/3.1.x.test.ts | 4 +- .../plugins/@hey-api/sdk/shared/operation.ts | 30 +++---------- .../@hey-api/sdk/shared/transformer.ts | 43 ++++++++++++------- .../plugins/@hey-api/sdk/shared/validator.ts | 6 ++- .../openapi-ts/src/plugins/arktype/api.ts | 10 +++-- .../openapi-ts/src/plugins/arktype/v2/api.ts | 6 ++- .../openapi-ts/src/plugins/valibot/api.ts | 10 +++-- .../openapi-ts/src/plugins/valibot/v1/api.ts | 14 +++--- packages/openapi-ts/src/plugins/zod/api.ts | 16 +++---- .../openapi-ts/src/plugins/zod/mini/api.ts | 16 +++---- packages/openapi-ts/src/plugins/zod/v3/api.ts | 16 +++---- packages/openapi-ts/src/plugins/zod/v4/api.ts | 16 +++---- packages/openapi-ts/src/ts-dsl/decl/func.ts | 1 + 14 files changed, 92 insertions(+), 97 deletions(-) diff --git a/packages/openapi-ts-tests/valibot/v1/test/3.1.x.test.ts b/packages/openapi-ts-tests/valibot/v1/test/3.1.x.test.ts index 13e5671aef..0a1e962992 100644 --- a/packages/openapi-ts-tests/valibot/v1/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/valibot/v1/test/3.1.x.test.ts @@ -164,7 +164,6 @@ describe(`OpenAPI ${version}`, () => { output: 'type-format', plugins: [ '@hey-api/transformers', - '@hey-api/client-fetch', 'valibot', { name: '@hey-api/sdk', diff --git a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts index ab080b99cd..79714ad4c9 100644 --- a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts @@ -227,12 +227,12 @@ for (const zodVersion of zodVersions) { }, { name: '@hey-api/sdk', - transformer: 'zod', + transformer: true, validator: true, }, ], }), - description: 'uses zod as response transformer', + description: 'handles various schema types and formats', }, ]; diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts index 9ad6bc49d7..268818c059 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts @@ -1,4 +1,3 @@ -import type { SymbolMeta } from '@hey-api/codegen-core'; import type { IR } from '@hey-api/shared'; import { statusCodeToGroup } from '@hey-api/shared'; @@ -335,32 +334,15 @@ export function operationStatements({ } const requestValidator = createRequestValidator({ operation, plugin }); + const responseTransformer = createResponseTransformer({ operation, plugin }); const responseValidator = createResponseValidator({ operation, plugin }); + if (requestValidator) { - reqOptions.prop('requestValidator', requestValidator.arrow()); + reqOptions.prop('requestValidator', requestValidator); } - if (plugin.config.transformer.response) { - const transformerPlugin = plugin.getPlugin(plugin.config.transformer.response); - if (transformerPlugin?.api && 'createResponseTransformer' in transformerPlugin.api) { - // Zod-style transformer: inline arrow function wrapping parseAsync - const responseTransformerFn = createResponseTransformer({ operation, plugin }); - if (responseTransformerFn) { - reqOptions.prop('responseTransformer', responseTransformerFn.arrow()); - } - } else { - // @hey-api/transformers-style: reference to a named transformer function - const query: SymbolMeta = { - category: 'transform', - resource: 'operation', - resourceId: operation.id, - role: 'response', - }; - if (plugin.isSymbolRegistered(query)) { - const ref = plugin.referenceSymbol(query); - reqOptions.prop('responseTransformer', $(ref)); - } - } + if (responseTransformer) { + reqOptions.prop('responseTransformer', responseTransformer); } let hasServerSentEvents = false; @@ -388,7 +370,7 @@ export function operationStatements({ } if (responseValidator) { - reqOptions.prop('responseValidator', responseValidator.arrow()); + reqOptions.prop('responseValidator', responseValidator); } if (plugin.config.responseStyle === 'data') { diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts index 062040cee8..d0d079c251 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts @@ -1,6 +1,7 @@ +import type { SymbolMeta } from '@hey-api/codegen-core'; import type { IR } from '@hey-api/shared'; -import type { $ } from '../../../../ts-dsl'; +import { $ } from '../../../../ts-dsl'; import type { HeyApiSdkPlugin } from '../types'; export function createResponseTransformer({ @@ -11,20 +12,32 @@ export function createResponseTransformer({ operation: IR.OperationObject; /** The plugin instance. */ plugin: HeyApiSdkPlugin['Instance']; -}): ReturnType | undefined { - const { response } = plugin.config.transformer; - if (!response) return; +}): + | Extract, { '~mode': 'arrow' }> + | ReturnType + | undefined { + if (!plugin.config.transformer.response) return; - const transformer = plugin.getPluginOrThrow(response); - if ( - !transformer.api || - !('createResponseTransformer' in transformer.api) || - typeof transformer.api.createResponseTransformer !== 'function' - ) - return; + const transformer = plugin.getPluginOrThrow(plugin.config.transformer.response); - return transformer.api.createResponseTransformer({ - operation, - plugin: transformer, - }); + if ( + transformer?.api?.createResponseTransformer && + typeof transformer.api.createResponseTransformer === 'function' + ) { + return transformer.api.createResponseTransformer({ + operation, + plugin: transformer, + }); + } else { + const query: SymbolMeta = { + category: 'transform', + resource: 'operation', + resourceId: operation.id, + role: 'response', + }; + if (plugin.isSymbolRegistered(query)) { + const ref = plugin.referenceSymbol(query); + return $(ref); + } + } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/validator.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/validator.ts index c66fac3303..a0756f6c6c 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/validator.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/validator.ts @@ -3,10 +3,12 @@ import type { IR, RequestSchemaContext } from '@hey-api/shared'; import type { $ } from '../../../../ts-dsl'; import type { HeyApiSdkPlugin } from '../types'; +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + export function createRequestValidator({ plugin, ...args -}: RequestSchemaContext): ReturnType | undefined { +}: RequestSchemaContext): ArrowFunc | undefined { if (!plugin.config.validator.request) return; const validator = plugin.getPluginOrThrow(plugin.config.validator.request); @@ -27,7 +29,7 @@ export function createResponseValidator({ operation: IR.OperationObject; /** The plugin instance. */ plugin: HeyApiSdkPlugin['Instance']; -}): ReturnType | undefined { +}): ArrowFunc | undefined { if (!plugin.config.validator.response) return; const validator = plugin.getPluginOrThrow(plugin.config.validator.response); diff --git a/packages/openapi-ts/src/plugins/arktype/api.ts b/packages/openapi-ts/src/plugins/arktype/api.ts index 08efb3b036..01a83c3bd1 100644 --- a/packages/openapi-ts/src/plugins/arktype/api.ts +++ b/packages/openapi-ts/src/plugins/arktype/api.ts @@ -5,21 +5,23 @@ import type { ValidatorArgs } from './shared/types'; import type { ArktypePlugin } from './types'; import { createRequestValidatorV2, createResponseValidatorV2 } from './v2/api'; +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + export type IApi = { createRequestValidator: ( args: RequestSchemaContext, - ) => ReturnType | undefined; - createResponseValidator: (args: ValidatorArgs) => ReturnType | undefined; + ) => ArrowFunc | undefined; + createResponseValidator: (args: ValidatorArgs) => ArrowFunc | undefined; }; export class Api implements IApi { createRequestValidator( args: RequestSchemaContext, - ): ReturnType | undefined { + ): ArrowFunc | undefined { return createRequestValidatorV2(args); } - createResponseValidator(args: ValidatorArgs): ReturnType | undefined { + createResponseValidator(args: ValidatorArgs): ArrowFunc | undefined { return createResponseValidatorV2(args); } } diff --git a/packages/openapi-ts/src/plugins/arktype/v2/api.ts b/packages/openapi-ts/src/plugins/arktype/v2/api.ts index 9fca3a4c92..a546c91421 100644 --- a/packages/openapi-ts/src/plugins/arktype/v2/api.ts +++ b/packages/openapi-ts/src/plugins/arktype/v2/api.ts @@ -4,10 +4,12 @@ import { $ } from '../../../ts-dsl'; import type { ValidatorArgs } from '../shared/types'; import type { ArktypePlugin } from '../types'; +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + export function createRequestValidatorV2({ operation, plugin, -}: RequestSchemaContext): ReturnType | undefined { +}: RequestSchemaContext): ArrowFunc | undefined { const symbol = plugin.querySymbol({ category: 'schema', resource: 'operation', @@ -41,7 +43,7 @@ export function createRequestValidatorV2({ export function createResponseValidatorV2({ operation, plugin, -}: ValidatorArgs): ReturnType | undefined { +}: ValidatorArgs): ArrowFunc | undefined { const symbol = plugin.querySymbol({ category: 'schema', resource: 'operation', diff --git a/packages/openapi-ts/src/plugins/valibot/api.ts b/packages/openapi-ts/src/plugins/valibot/api.ts index 47d0b227f2..d519a33ada 100644 --- a/packages/openapi-ts/src/plugins/valibot/api.ts +++ b/packages/openapi-ts/src/plugins/valibot/api.ts @@ -11,14 +11,16 @@ import { createResponseValidatorV1, } from './v1/api'; +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + export type IApi = { createRequestSchema: ( ctx: RequestSchemaContext, ) => Symbol | Pipe | undefined; createRequestValidator: ( args: RequestSchemaContext, - ) => ReturnType | undefined; - createResponseValidator: (args: ValidatorArgs) => ReturnType | undefined; + ) => ArrowFunc | undefined; + createResponseValidator: (args: ValidatorArgs) => ArrowFunc | undefined; }; export class Api implements IApi { @@ -32,13 +34,13 @@ export class Api implements IApi { createRequestValidator( args: RequestSchemaContext, - ): ReturnType | undefined { + ): ArrowFunc | undefined { const { plugin } = args; if (!plugin.config.requests.enabled) return; return createRequestValidatorV1(args); } - createResponseValidator(args: ValidatorArgs): ReturnType | undefined { + createResponseValidator(args: ValidatorArgs): ArrowFunc | undefined { return createResponseValidatorV1(args); } } diff --git a/packages/openapi-ts/src/plugins/valibot/v1/api.ts b/packages/openapi-ts/src/plugins/valibot/v1/api.ts index 8fadfaf35d..8142e92c1a 100644 --- a/packages/openapi-ts/src/plugins/valibot/v1/api.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/api.ts @@ -15,6 +15,8 @@ import { getDefaultRequestValidatorLayers } from '../shared/validator'; import type { ValibotPlugin } from '../types'; import { identifiers } from './constants'; +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + function emptyNode( ctx: RequestValidatorResolverContext & { layer: ResolvedRequestValidatorLayer; @@ -93,9 +95,7 @@ function responseValidatorResolver( return $(v).attr(identifiers.async.parseAsync).call(schema, 'data').await().return(); } -function runRequestResolver( - ctx: RequestValidatorResolverContext, -): ReturnType | undefined { +function runRequestResolver(ctx: RequestValidatorResolverContext): ArrowFunc | undefined { const validator = ctx.plugin.config['~resolvers']?.validator; const resolver = typeof validator === 'function' ? validator : validator?.request; const candidates = [resolver, requestValidatorResolver]; @@ -111,9 +111,7 @@ function runRequestResolver( } } -function runResponseResolver( - ctx: ResponseValidatorResolverContext, -): ReturnType | undefined { +function runResponseResolver(ctx: ResponseValidatorResolverContext): ArrowFunc | undefined { const validator = ctx.plugin.config['~resolvers']?.validator; const resolver = typeof validator === 'function' ? validator : validator?.response; const candidates = [resolver, responseValidatorResolver]; @@ -187,7 +185,7 @@ export function createRequestSchemaV1( export function createRequestValidatorV1( ctx: RequestSchemaContext, -): ReturnType | undefined { +): ArrowFunc | undefined { const symbolOrSchema = createRequestSchemaV1(ctx); if (!symbolOrSchema) return; @@ -205,7 +203,7 @@ export function createRequestValidatorV1( export function createResponseValidatorV1({ operation, plugin, -}: ValidatorArgs): ReturnType | undefined { +}: ValidatorArgs): ArrowFunc | undefined { const symbol = plugin.querySymbol({ category: 'schema', resource: 'operation', diff --git a/packages/openapi-ts/src/plugins/zod/api.ts b/packages/openapi-ts/src/plugins/zod/api.ts index cdc3e3e680..39f81866ec 100644 --- a/packages/openapi-ts/src/plugins/zod/api.ts +++ b/packages/openapi-ts/src/plugins/zod/api.ts @@ -24,15 +24,17 @@ import { createResponseValidatorV4, } from './v4/api'; +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + export type IApi = { createRequestSchema: ( ctx: RequestSchemaContext, ) => Symbol | Chain | undefined; createRequestValidator: ( ctx: RequestSchemaContext, - ) => ReturnType | undefined; - createResponseTransformer: (ctx: ValidatorArgs) => ReturnType | undefined; - createResponseValidator: (ctx: ValidatorArgs) => ReturnType | undefined; + ) => ArrowFunc | undefined; + createResponseTransformer: (ctx: ValidatorArgs) => ArrowFunc | undefined; + createResponseValidator: (ctx: ValidatorArgs) => ArrowFunc | undefined; }; export class Api implements IApi { @@ -52,9 +54,7 @@ export class Api implements IApi { } } - createRequestValidator( - ctx: RequestSchemaContext, - ): ReturnType | undefined { + createRequestValidator(ctx: RequestSchemaContext): ArrowFunc | undefined { const { plugin } = ctx; if (!plugin.config.requests.enabled) return; switch (plugin.config.compatibilityVersion) { @@ -68,7 +68,7 @@ export class Api implements IApi { } } - createResponseTransformer(ctx: ValidatorArgs): ReturnType | undefined { + createResponseTransformer(ctx: ValidatorArgs): ArrowFunc | undefined { const { plugin } = ctx; switch (plugin.config.compatibilityVersion) { case 3: @@ -81,7 +81,7 @@ export class Api implements IApi { } } - createResponseValidator(ctx: ValidatorArgs): ReturnType | undefined { + createResponseValidator(ctx: ValidatorArgs): ArrowFunc | undefined { const { plugin } = ctx; switch (plugin.config.compatibilityVersion) { case 3: diff --git a/packages/openapi-ts/src/plugins/zod/mini/api.ts b/packages/openapi-ts/src/plugins/zod/mini/api.ts index ec45a2a4b2..bbeb438037 100644 --- a/packages/openapi-ts/src/plugins/zod/mini/api.ts +++ b/packages/openapi-ts/src/plugins/zod/mini/api.ts @@ -15,6 +15,8 @@ import type { ValidatorArgs } from '../shared/types'; import { getDefaultRequestValidatorLayers } from '../shared/validator'; import type { ZodPlugin } from '../types'; +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + function emptyNode( ctx: RequestValidatorResolverContext & { layer: ResolvedRequestValidatorLayer; @@ -93,9 +95,7 @@ function responseValidatorResolver( return $(schema).attr(identifiers.parseAsync).call('data').await().return(); } -function runRequestResolver( - ctx: RequestValidatorResolverContext, -): ReturnType | undefined { +function runRequestResolver(ctx: RequestValidatorResolverContext): ArrowFunc | undefined { const validator = ctx.plugin.config['~resolvers']?.validator; const resolver = typeof validator === 'function' ? validator : validator?.request; const candidates = [resolver, requestValidatorResolver]; @@ -111,9 +111,7 @@ function runRequestResolver( } } -function runResponseResolver( - ctx: ResponseValidatorResolverContext, -): ReturnType | undefined { +function runResponseResolver(ctx: ResponseValidatorResolverContext): ArrowFunc | undefined { const validator = ctx.plugin.config['~resolvers']?.validator; const resolver = typeof validator === 'function' ? validator : validator?.response; const candidates = [resolver, responseValidatorResolver]; @@ -186,7 +184,7 @@ export function createRequestSchemaMini( export function createRequestValidatorMini( ctx: RequestSchemaContext, -): ReturnType | undefined { +): ArrowFunc | undefined { const symbolOrSchema = createRequestSchemaMini(ctx); if (!symbolOrSchema) return; @@ -204,7 +202,7 @@ export function createRequestValidatorMini( export function createResponseValidatorMini({ operation, plugin, -}: ValidatorArgs): ReturnType | undefined { +}: ValidatorArgs): ArrowFunc | undefined { const symbol = plugin.querySymbol({ category: 'schema', resource: 'operation', @@ -234,6 +232,6 @@ export function createResponseValidatorMini({ export function createResponseTransformerMini({ operation, plugin, -}: ValidatorArgs): ReturnType | undefined { +}: ValidatorArgs): ArrowFunc | undefined { return createResponseValidatorMini({ operation, plugin }); } diff --git a/packages/openapi-ts/src/plugins/zod/v3/api.ts b/packages/openapi-ts/src/plugins/zod/v3/api.ts index 75091c9c83..cae6ce9710 100644 --- a/packages/openapi-ts/src/plugins/zod/v3/api.ts +++ b/packages/openapi-ts/src/plugins/zod/v3/api.ts @@ -15,6 +15,8 @@ import type { ValidatorArgs } from '../shared/types'; import { getDefaultRequestValidatorLayers } from '../shared/validator'; import type { ZodPlugin } from '../types'; +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + function emptyNode( ctx: RequestValidatorResolverContext & { layer: ResolvedRequestValidatorLayer; @@ -92,9 +94,7 @@ function responseValidatorResolver( return $(schema).attr(identifiers.parseAsync).call('data').await().return(); } -function runRequestResolver( - ctx: RequestValidatorResolverContext, -): ReturnType | undefined { +function runRequestResolver(ctx: RequestValidatorResolverContext): ArrowFunc | undefined { const validator = ctx.plugin.config['~resolvers']?.validator; const resolver = typeof validator === 'function' ? validator : validator?.request; const candidates = [resolver, requestValidatorResolver]; @@ -110,9 +110,7 @@ function runRequestResolver( } } -function runResponseResolver( - ctx: ResponseValidatorResolverContext, -): ReturnType | undefined { +function runResponseResolver(ctx: ResponseValidatorResolverContext): ArrowFunc | undefined { const validator = ctx.plugin.config['~resolvers']?.validator; const resolver = typeof validator === 'function' ? validator : validator?.response; const candidates = [resolver, responseValidatorResolver]; @@ -185,7 +183,7 @@ export function createRequestSchemaV3( export function createRequestValidatorV3( ctx: RequestSchemaContext, -): ReturnType | undefined { +): ArrowFunc | undefined { const symbolOrSchema = createRequestSchemaV3(ctx); if (!symbolOrSchema) return; @@ -203,7 +201,7 @@ export function createRequestValidatorV3( export function createResponseValidatorV3({ operation, plugin, -}: ValidatorArgs): ReturnType | undefined { +}: ValidatorArgs): ArrowFunc | undefined { const symbol = plugin.querySymbol({ category: 'schema', resource: 'operation', @@ -233,6 +231,6 @@ export function createResponseValidatorV3({ export function createResponseTransformerV3({ operation, plugin, -}: ValidatorArgs): ReturnType | undefined { +}: ValidatorArgs): ArrowFunc | undefined { return createResponseValidatorV3({ operation, plugin }); } diff --git a/packages/openapi-ts/src/plugins/zod/v4/api.ts b/packages/openapi-ts/src/plugins/zod/v4/api.ts index 79232c44e4..2d4249c3fd 100644 --- a/packages/openapi-ts/src/plugins/zod/v4/api.ts +++ b/packages/openapi-ts/src/plugins/zod/v4/api.ts @@ -15,6 +15,8 @@ import type { ValidatorArgs } from '../shared/types'; import { getDefaultRequestValidatorLayers } from '../shared/validator'; import type { ZodPlugin } from '../types'; +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + function emptyNode( ctx: RequestValidatorResolverContext & { layer: ResolvedRequestValidatorLayer; @@ -92,9 +94,7 @@ function responseValidatorResolver( return $(schema).attr(identifiers.parseAsync).call('data').await().return(); } -function runRequestResolver( - ctx: RequestValidatorResolverContext, -): ReturnType | undefined { +function runRequestResolver(ctx: RequestValidatorResolverContext): ArrowFunc | undefined { const validator = ctx.plugin.config['~resolvers']?.validator; const resolver = typeof validator === 'function' ? validator : validator?.request; const candidates = [resolver, requestValidatorResolver]; @@ -110,9 +110,7 @@ function runRequestResolver( } } -function runResponseResolver( - ctx: ResponseValidatorResolverContext, -): ReturnType | undefined { +function runResponseResolver(ctx: ResponseValidatorResolverContext): ArrowFunc | undefined { const validator = ctx.plugin.config['~resolvers']?.validator; const resolver = typeof validator === 'function' ? validator : validator?.response; const candidates = [resolver, responseValidatorResolver]; @@ -185,7 +183,7 @@ export function createRequestSchemaV4( export function createRequestValidatorV4( ctx: RequestSchemaContext, -): ReturnType | undefined { +): ArrowFunc | undefined { const symbolOrSchema = createRequestSchemaV4(ctx); if (!symbolOrSchema) return; @@ -203,7 +201,7 @@ export function createRequestValidatorV4( export function createResponseValidatorV4({ operation, plugin, -}: ValidatorArgs): ReturnType | undefined { +}: ValidatorArgs): ArrowFunc | undefined { const symbol = plugin.querySymbol({ category: 'schema', resource: 'operation', @@ -233,6 +231,6 @@ export function createResponseValidatorV4({ export function createResponseTransformerV4({ operation, plugin, -}: ValidatorArgs): ReturnType | undefined { +}: ValidatorArgs): ArrowFunc | undefined { return createResponseValidatorV4({ operation, plugin }); } diff --git a/packages/openapi-ts/src/ts-dsl/decl/func.ts b/packages/openapi-ts/src/ts-dsl/decl/func.ts index 903a584623..9c03e5262a 100644 --- a/packages/openapi-ts/src/ts-dsl/decl/func.ts +++ b/packages/openapi-ts/src/ts-dsl/decl/func.ts @@ -50,6 +50,7 @@ const Mixed = AbstractMixin( class ImplFuncTsDsl extends Mixed { readonly '~dsl' = 'FuncTsDsl'; + readonly '~mode'!: M; // type-only brand, never assigned at runtime override readonly nameSanitizer = safeRuntimeName; protected mode?: FuncMode; From 2c00b324b0bc730c47d789309e05f3491bd6b520 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 16 May 2026 12:49:56 +0800 Subject: [PATCH 11/14] chore: support valibot as transformer --- dev/typescript/presets.ts | 12 +++++- .../3.1.x/mini/transformer/sdk.gen.ts | 1 - .../3.1.x/v3/transformer/sdk.gen.ts | 1 - .../3.1.x/v4/transformer/sdk.gen.ts | 1 - .../plugins/@hey-api/sdk/shared/handlers.ts | 27 +++++++++++++ .../plugins/@hey-api/sdk/shared/operation.ts | 15 ++++---- .../@hey-api/sdk/shared/transformer.ts | 38 +++++++------------ .../src/plugins/@hey-api/sdk/shared/types.ts | 18 +++++++++ .../plugins/@hey-api/sdk/shared/validator.ts | 10 ++--- packages/openapi-ts/src/plugins/types.ts | 2 +- .../openapi-ts/src/plugins/valibot/api.ts | 33 +++++++++++----- .../openapi-ts/src/plugins/valibot/config.ts | 2 +- .../openapi-ts/src/plugins/valibot/v1/api.ts | 14 +++++++ packages/openapi-ts/src/plugins/zod/api.ts | 30 +++++++++++++-- .../openapi-ts/src/plugins/zod/mini/api.ts | 17 ++++++--- packages/openapi-ts/src/plugins/zod/v3/api.ts | 17 ++++++--- packages/openapi-ts/src/plugins/zod/v4/api.ts | 17 ++++++--- .../openapi/typescript/plugins/zod/v4.mdx | 18 --------- 18 files changed, 182 insertions(+), 91 deletions(-) create mode 100644 packages/openapi-ts/src/plugins/@hey-api/sdk/shared/handlers.ts create mode 100644 packages/openapi-ts/src/plugins/@hey-api/sdk/shared/types.ts diff --git a/dev/typescript/presets.ts b/dev/typescript/presets.ts index 4a38c5cc1e..e8c21d6d5e 100644 --- a/dev/typescript/presets.ts +++ b/dev/typescript/presets.ts @@ -58,12 +58,22 @@ export const presets = { }, }, ], + transformed: () => [ + /** SDK + transforms */ + '@hey-api/typescript', + { + name: '@hey-api/sdk', + transformer: 'valibot', + }, + 'valibot', + 'zod', + ], types: () => [ /** Just types, nothing else */ '@hey-api/typescript', ], validated: () => [ - /** SDK + Zod validation */ + /** SDK + validation */ '@hey-api/typescript', { name: '@hey-api/sdk', diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/sdk.gen.ts index f741139140..0c0e6947a0 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/sdk.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/sdk.gen.ts @@ -28,7 +28,6 @@ export const postFoo = (options?: Options< query: z.optional(z.never()) }).parseAsync(data), responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), - responseValidator: async (data) => await zPostFooResponse.parseAsync(data), url: '/foo', ...options }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/sdk.gen.ts index f95fb60307..c3cf00d20b 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/sdk.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/sdk.gen.ts @@ -28,7 +28,6 @@ export const postFoo = (options?: Options< query: z.never().optional() }).parseAsync(data), responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), - responseValidator: async (data) => await zPostFooResponse.parseAsync(data), url: '/foo', ...options }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/sdk.gen.ts index 2a0fe9d412..ab9d05b5ba 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/sdk.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/sdk.gen.ts @@ -28,7 +28,6 @@ export const postFoo = (options?: Options< query: z.never().optional() }).parseAsync(data), responseTransformer: async (data) => await zPostFooResponse.parseAsync(data), - responseValidator: async (data) => await zPostFooResponse.parseAsync(data), url: '/foo', ...options }); diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/handlers.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/handlers.ts new file mode 100644 index 0000000000..0fa650ef89 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/handlers.ts @@ -0,0 +1,27 @@ +import { createResponseTransformer } from './transformer'; +import type { ResponseHandlers, ValidatorArgs } from './types'; +import { createResponseValidator } from './validator'; + +export function createResponseHandlers({ operation, plugin }: ValidatorArgs): ResponseHandlers { + let handlers: ResponseHandlers | undefined; + const responseTransformer = plugin.config.transformer.response; + const useResponseHandlers = + responseTransformer && responseTransformer === plugin.config.validator.response; + if (useResponseHandlers) { + const handler = plugin.getPluginOrThrow(responseTransformer); + if (handler.api?.createResponseHandlers) { + handlers = handler.api.createResponseHandlers({ + operation, + // @ts-expect-error + plugin: handler, + }); + } + } + if (!handlers) { + handlers = { + transformer: createResponseTransformer({ operation, plugin }), + validator: createResponseValidator({ operation, plugin }), + }; + } + return handlers; +} diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts index 268818c059..a2f3ce27aa 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts @@ -10,9 +10,9 @@ import type { HeyApiSdkPlugin } from '../types'; import { isInstance } from '../v1/node'; import { operationAuth } from './auth'; import { nuxtTypeComposable, nuxtTypeDefault } from './constants'; +import { createResponseHandlers } from './handlers'; import { getSignatureParameters } from './signature'; -import { createResponseTransformer } from './transformer'; -import { createRequestValidator, createResponseValidator } from './validator'; +import { createRequestValidator } from './validator'; /** TODO: needs complete refactor */ export const operationOptionsType = ({ @@ -334,15 +334,14 @@ export function operationStatements({ } const requestValidator = createRequestValidator({ operation, plugin }); - const responseTransformer = createResponseTransformer({ operation, plugin }); - const responseValidator = createResponseValidator({ operation, plugin }); + const responseHandlers = createResponseHandlers({ operation, plugin }); if (requestValidator) { reqOptions.prop('requestValidator', requestValidator); } - if (responseTransformer) { - reqOptions.prop('responseTransformer', responseTransformer); + if (responseHandlers.transformer) { + reqOptions.prop('responseTransformer', responseHandlers.transformer); } let hasServerSentEvents = false; @@ -369,8 +368,8 @@ export function operationStatements({ } } - if (responseValidator) { - reqOptions.prop('responseValidator', responseValidator); + if (responseHandlers.validator) { + reqOptions.prop('responseValidator', responseHandlers.validator); } if (plugin.config.responseStyle === 'data') { diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts index d0d079c251..f4f1af7dd2 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts @@ -1,43 +1,33 @@ import type { SymbolMeta } from '@hey-api/codegen-core'; -import type { IR } from '@hey-api/shared'; import { $ } from '../../../../ts-dsl'; -import type { HeyApiSdkPlugin } from '../types'; +import type { ResponseHandlers, ValidatorArgs } from './types'; export function createResponseTransformer({ operation, plugin, -}: { - /** The operation object. */ - operation: IR.OperationObject; - /** The plugin instance. */ - plugin: HeyApiSdkPlugin['Instance']; -}): - | Extract, { '~mode': 'arrow' }> - | ReturnType - | undefined { +}: ValidatorArgs): ResponseHandlers['transformer'] { if (!plugin.config.transformer.response) return; const transformer = plugin.getPluginOrThrow(plugin.config.transformer.response); - if ( - transformer?.api?.createResponseTransformer && + transformer.api?.createResponseTransformer && typeof transformer.api.createResponseTransformer === 'function' ) { return transformer.api.createResponseTransformer({ operation, plugin: transformer, }); - } else { - const query: SymbolMeta = { - category: 'transform', - resource: 'operation', - resourceId: operation.id, - role: 'response', - }; - if (plugin.isSymbolRegistered(query)) { - const ref = plugin.referenceSymbol(query); - return $(ref); - } + } + + const query: SymbolMeta = { + category: 'transform', + resource: 'operation', + resourceId: operation.id, + role: 'response', + }; + if (plugin.isSymbolRegistered(query)) { + const ref = plugin.referenceSymbol(query); + return $(ref); } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/types.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/types.ts new file mode 100644 index 0000000000..a1c2f13a52 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/types.ts @@ -0,0 +1,18 @@ +import type { IR } from '@hey-api/shared'; + +import type { $ } from '../../../../ts-dsl'; +import type { HeyApiSdkPlugin } from '../types'; + +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + +export type ResponseHandlers = { + transformer: ArrowFunc | ReturnType | undefined; + validator: ArrowFunc | undefined; +}; + +export type ValidatorArgs = { + /** The operation object. */ + operation: IR.OperationObject; + /** The plugin instance. */ + plugin: HeyApiSdkPlugin['Instance']; +}; diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/validator.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/validator.ts index a0756f6c6c..244634d353 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/validator.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/validator.ts @@ -1,7 +1,8 @@ -import type { IR, RequestSchemaContext } from '@hey-api/shared'; +import type { RequestSchemaContext } from '@hey-api/shared'; import type { $ } from '../../../../ts-dsl'; import type { HeyApiSdkPlugin } from '../types'; +import type { ResponseHandlers, ValidatorArgs } from './types'; type ArrowFunc = Extract, { '~mode': 'arrow' }>; @@ -24,12 +25,7 @@ export function createRequestValidator({ export function createResponseValidator({ operation, plugin, -}: { - /** The operation object. */ - operation: IR.OperationObject; - /** The plugin instance. */ - plugin: HeyApiSdkPlugin['Instance']; -}): ArrowFunc | undefined { +}: ValidatorArgs): ResponseHandlers['validator'] { if (!plugin.config.validator.response) return; const validator = plugin.getPluginOrThrow(plugin.config.validator.response); diff --git a/packages/openapi-ts/src/plugins/types.ts b/packages/openapi-ts/src/plugins/types.ts index 3a1d22f1d2..c803e8e70b 100644 --- a/packages/openapi-ts/src/plugins/types.ts +++ b/packages/openapi-ts/src/plugins/types.ts @@ -9,6 +9,6 @@ export type PluginClientNames = export type PluginMockNames = '@faker-js/faker'; -export type PluginTransformerNames = '@hey-api/transformers' | 'zod'; +export type PluginTransformerNames = '@hey-api/transformers' | 'valibot' | 'zod'; export type PluginValidatorNames = 'arktype' | 'valibot' | 'zod'; diff --git a/packages/openapi-ts/src/plugins/valibot/api.ts b/packages/openapi-ts/src/plugins/valibot/api.ts index d519a33ada..0ae51dceff 100644 --- a/packages/openapi-ts/src/plugins/valibot/api.ts +++ b/packages/openapi-ts/src/plugins/valibot/api.ts @@ -8,6 +8,8 @@ import type { ValibotPlugin } from './types'; import { createRequestSchemaV1, createRequestValidatorV1, + createResponseHandlersV1, + createResponseTransformerV1, createResponseValidatorV1, } from './v1/api'; @@ -18,29 +20,42 @@ export type IApi = { ctx: RequestSchemaContext, ) => Symbol | Pipe | undefined; createRequestValidator: ( - args: RequestSchemaContext, + ctx: RequestSchemaContext, ) => ArrowFunc | undefined; - createResponseValidator: (args: ValidatorArgs) => ArrowFunc | undefined; + createResponseHandlers: (ctx: ValidatorArgs) => { + transformer: ArrowFunc | undefined; + validator: ArrowFunc | undefined; + }; + createResponseTransformer: (ctx: ValidatorArgs) => ArrowFunc | undefined; + createResponseValidator: (ctx: ValidatorArgs) => ArrowFunc | undefined; }; export class Api implements IApi { createRequestSchema( ctx: RequestSchemaContext, - ): Symbol | Pipe | undefined { + ): ReturnType { const { plugin } = ctx; if (!plugin.config.requests.enabled) return; return createRequestSchemaV1(ctx); } createRequestValidator( - args: RequestSchemaContext, - ): ArrowFunc | undefined { - const { plugin } = args; + ctx: RequestSchemaContext, + ): ReturnType { + const { plugin } = ctx; if (!plugin.config.requests.enabled) return; - return createRequestValidatorV1(args); + return createRequestValidatorV1(ctx); + } + + createResponseHandlers(ctx: ValidatorArgs): ReturnType { + return createResponseHandlersV1(ctx); + } + + createResponseTransformer(ctx: ValidatorArgs): ReturnType { + return createResponseTransformerV1(ctx); } - createResponseValidator(args: ValidatorArgs): ArrowFunc | undefined { - return createResponseValidatorV1(args); + createResponseValidator(ctx: ValidatorArgs): ReturnType { + return createResponseValidatorV1(ctx); } } diff --git a/packages/openapi-ts/src/plugins/valibot/config.ts b/packages/openapi-ts/src/plugins/valibot/config.ts index 0fb126ce09..5f4a8a96cc 100644 --- a/packages/openapi-ts/src/plugins/valibot/config.ts +++ b/packages/openapi-ts/src/plugins/valibot/config.ts @@ -119,7 +119,7 @@ export const defaultConfig: ValibotPlugin['Config'] = { value: plugin.config.webhooks, }); }, - tags: ['validator'], + tags: ['transformer', 'validator'], }; /** diff --git a/packages/openapi-ts/src/plugins/valibot/v1/api.ts b/packages/openapi-ts/src/plugins/valibot/v1/api.ts index 8142e92c1a..1d89a0ada4 100644 --- a/packages/openapi-ts/src/plugins/valibot/v1/api.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/api.ts @@ -230,3 +230,17 @@ export function createResponseValidatorV1({ }; return runResponseResolver(resolverCtx); } + +export function createResponseTransformerV1(ctx: ValidatorArgs): ArrowFunc | undefined { + return createResponseValidatorV1(ctx); +} + +export function createResponseHandlersV1(ctx: ValidatorArgs): { + transformer: ArrowFunc | undefined; + validator: ArrowFunc | undefined; +} { + return { + transformer: createResponseTransformerV1(ctx), + validator: undefined, + }; +} diff --git a/packages/openapi-ts/src/plugins/zod/api.ts b/packages/openapi-ts/src/plugins/zod/api.ts index 39f81866ec..bc1ba1824a 100644 --- a/packages/openapi-ts/src/plugins/zod/api.ts +++ b/packages/openapi-ts/src/plugins/zod/api.ts @@ -5,6 +5,7 @@ import type { $ } from '../../ts-dsl'; import { createRequestSchemaMini, createRequestValidatorMini, + createResponseHandlersMini, createResponseTransformerMini, createResponseValidatorMini, } from './mini/api'; @@ -14,12 +15,14 @@ import type { ZodPlugin } from './types'; import { createRequestSchemaV3, createRequestValidatorV3, + createResponseHandlersV3, createResponseTransformerV3, createResponseValidatorV3, } from './v3/api'; import { createRequestSchemaV4, createRequestValidatorV4, + createResponseHandlersV4, createResponseTransformerV4, createResponseValidatorV4, } from './v4/api'; @@ -33,6 +36,10 @@ export type IApi = { createRequestValidator: ( ctx: RequestSchemaContext, ) => ArrowFunc | undefined; + createResponseHandlers: (ctx: ValidatorArgs) => { + transformer: ArrowFunc | undefined; + validator: ArrowFunc | undefined; + }; createResponseTransformer: (ctx: ValidatorArgs) => ArrowFunc | undefined; createResponseValidator: (ctx: ValidatorArgs) => ArrowFunc | undefined; }; @@ -40,7 +47,7 @@ export type IApi = { export class Api implements IApi { createRequestSchema( ctx: RequestSchemaContext, - ): Symbol | Chain | undefined { + ): ReturnType { const { plugin } = ctx; if (!plugin.config.requests.enabled) return; switch (plugin.config.compatibilityVersion) { @@ -54,7 +61,9 @@ export class Api implements IApi { } } - createRequestValidator(ctx: RequestSchemaContext): ArrowFunc | undefined { + createRequestValidator( + ctx: RequestSchemaContext, + ): ReturnType { const { plugin } = ctx; if (!plugin.config.requests.enabled) return; switch (plugin.config.compatibilityVersion) { @@ -68,7 +77,20 @@ export class Api implements IApi { } } - createResponseTransformer(ctx: ValidatorArgs): ArrowFunc | undefined { + createResponseHandlers(ctx: ValidatorArgs): ReturnType { + const { plugin } = ctx; + switch (plugin.config.compatibilityVersion) { + case 3: + return createResponseHandlersV3(ctx); + case 'mini': + return createResponseHandlersMini(ctx); + case 4: + default: + return createResponseHandlersV4(ctx); + } + } + + createResponseTransformer(ctx: ValidatorArgs): ReturnType { const { plugin } = ctx; switch (plugin.config.compatibilityVersion) { case 3: @@ -81,7 +103,7 @@ export class Api implements IApi { } } - createResponseValidator(ctx: ValidatorArgs): ArrowFunc | undefined { + createResponseValidator(ctx: ValidatorArgs): ReturnType { const { plugin } = ctx; switch (plugin.config.compatibilityVersion) { case 3: diff --git a/packages/openapi-ts/src/plugins/zod/mini/api.ts b/packages/openapi-ts/src/plugins/zod/mini/api.ts index bbeb438037..49dd7165a0 100644 --- a/packages/openapi-ts/src/plugins/zod/mini/api.ts +++ b/packages/openapi-ts/src/plugins/zod/mini/api.ts @@ -229,9 +229,16 @@ export function createResponseValidatorMini({ return runResponseResolver(resolverCtx); } -export function createResponseTransformerMini({ - operation, - plugin, -}: ValidatorArgs): ArrowFunc | undefined { - return createResponseValidatorMini({ operation, plugin }); +export function createResponseTransformerMini(ctx: ValidatorArgs): ArrowFunc | undefined { + return createResponseValidatorMini(ctx); +} + +export function createResponseHandlersMini(ctx: ValidatorArgs): { + transformer: ArrowFunc | undefined; + validator: ArrowFunc | undefined; +} { + return { + transformer: createResponseTransformerMini(ctx), + validator: undefined, + }; } diff --git a/packages/openapi-ts/src/plugins/zod/v3/api.ts b/packages/openapi-ts/src/plugins/zod/v3/api.ts index cae6ce9710..1fbc92d7f5 100644 --- a/packages/openapi-ts/src/plugins/zod/v3/api.ts +++ b/packages/openapi-ts/src/plugins/zod/v3/api.ts @@ -228,9 +228,16 @@ export function createResponseValidatorV3({ return runResponseResolver(resolverCtx); } -export function createResponseTransformerV3({ - operation, - plugin, -}: ValidatorArgs): ArrowFunc | undefined { - return createResponseValidatorV3({ operation, plugin }); +export function createResponseTransformerV3(ctx: ValidatorArgs): ArrowFunc | undefined { + return createResponseValidatorV3(ctx); +} + +export function createResponseHandlersV3(ctx: ValidatorArgs): { + transformer: ArrowFunc | undefined; + validator: ArrowFunc | undefined; +} { + return { + transformer: createResponseTransformerV3(ctx), + validator: undefined, + }; } diff --git a/packages/openapi-ts/src/plugins/zod/v4/api.ts b/packages/openapi-ts/src/plugins/zod/v4/api.ts index 2d4249c3fd..f3fcf803f9 100644 --- a/packages/openapi-ts/src/plugins/zod/v4/api.ts +++ b/packages/openapi-ts/src/plugins/zod/v4/api.ts @@ -228,9 +228,16 @@ export function createResponseValidatorV4({ return runResponseResolver(resolverCtx); } -export function createResponseTransformerV4({ - operation, - plugin, -}: ValidatorArgs): ArrowFunc | undefined { - return createResponseValidatorV4({ operation, plugin }); +export function createResponseTransformerV4(ctx: ValidatorArgs): ArrowFunc | undefined { + return createResponseValidatorV4(ctx); +} + +export function createResponseHandlersV4(ctx: ValidatorArgs): { + transformer: ArrowFunc | undefined; + validator: ArrowFunc | undefined; +} { + return { + transformer: createResponseTransformerV4(ctx), + validator: undefined, + }; } diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx index 21bd2af2e5..1020a4b88f 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx @@ -92,24 +92,6 @@ export const getFoo = (options?) => }); ``` -::: warning Avoid duplicate `parseAsync` calls -When `transformer: 'zod'` is enabled, `parseAsync` is already called on the response as part of transformation. If you also enable `validator: true` (or `validator: { response: true }`), `parseAsync` will be called a **second time** for response validation, which is redundant and adds unnecessary overhead. - -To avoid this, disable response validation when using Zod as a transformer: - -```js -{ - name: '@hey-api/sdk', - transformer: 'zod', // [!code ++] - validator: { // [!code ++] - request: true, // still validate outgoing requests // [!code ++] - response: false, // skip — transformer already runs parseAsync // [!code ++] - }, // [!code ++] -} -``` - -::: - ## Output The Zod plugin will generate the following artifacts, depending on the input specification. From 6770f78dec53b1bf8f37c361c7b9c2e2eb4157fa Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 16 May 2026 20:55:29 +0800 Subject: [PATCH 12/14] test: update snapshots --- .changeset/common-hairs-add.md | 5 +++ .changeset/heavy-states-cheat.md | 5 +++ .../3.1.x/type-format/sdk.gen.ts | 4 +- .../3.1.x/type-format/types.gen.ts | 2 +- .../valibot/v1/test/3.1.x.test.ts | 1 - .../docs/docs/openapi/typescript/clients.mdx | 22 +++++------ .../typescript/clients/angular/v19.mdx | 6 +-- .../typescript/clients/angular/v20.mdx | 6 +-- .../docs/openapi/typescript/clients/axios.mdx | 4 +- .../docs/openapi/typescript/clients/fetch.mdx | 4 +- .../docs/openapi/typescript/clients/ky.mdx | 4 +- .../openapi/typescript/clients/next-js.mdx | 4 +- .../docs/openapi/typescript/clients/nuxt.mdx | 4 +- .../openapi/typescript/clients/ofetch.mdx | 4 +- .../typescript/community/contributing.mdx | 4 +- .../typescript/community/spotlight.mdx | 2 +- .../docs/openapi/typescript/configuration.mdx | 10 ++--- .../typescript/configuration/input.mdx | 4 +- .../typescript/configuration/output.mdx | 2 +- .../typescript/configuration/parser.mdx | 4 +- .../openapi/typescript/configuration/vite.mdx | 4 +- .../docs/docs/openapi/typescript/core.mdx | 8 ++-- .../docs/openapi/typescript/get-started.mdx | 12 +++--- .../docs/openapi/typescript/migrating.mdx | 18 ++++----- .../docs/docs/openapi/typescript/mocks.mdx | 12 +++--- .../docs/docs/openapi/typescript/output.mdx | 8 ++-- .../typescript/plugins/angular/v19.mdx | 2 +- .../typescript/plugins/angular/v20.mdx | 2 +- .../typescript/plugins/concepts/resolvers.mdx | 2 +- .../openapi/typescript/plugins/custom.mdx | 4 +- .../openapi/typescript/plugins/fastify.mdx | 2 +- .../docs/openapi/typescript/plugins/nest.mdx | 2 +- .../docs/openapi/typescript/plugins/orpc.mdx | 6 +-- .../typescript/plugins/pinia-colada.mdx | 10 ++--- .../docs/openapi/typescript/plugins/sdk.mdx | 12 ++++-- .../typescript/plugins/tanstack-query.mdx | 8 ++-- .../typescript/plugins/transformers.mdx | 8 ++-- .../openapi/typescript/plugins/typescript.mdx | 4 +- .../openapi/typescript/plugins/valibot.mdx | 30 +++++++++++--- .../openapi/typescript/plugins/zod/mini.mdx | 35 ++++++++++++++--- .../openapi/typescript/plugins/zod/v3.mdx | 35 ++++++++++++++--- .../openapi/typescript/plugins/zod/v4.mdx | 39 +++++++------------ .../openapi/typescript/state-management.mdx | 8 ++-- .../docs/openapi/typescript/validators.mdx | 14 +++---- .../openapi/typescript/web-frameworks.mdx | 18 ++++----- 45 files changed, 236 insertions(+), 168 deletions(-) create mode 100644 .changeset/common-hairs-add.md create mode 100644 .changeset/heavy-states-cheat.md diff --git a/.changeset/common-hairs-add.md b/.changeset/common-hairs-add.md new file mode 100644 index 0000000000..c2323a6bf0 --- /dev/null +++ b/.changeset/common-hairs-add.md @@ -0,0 +1,5 @@ +--- +"@hey-api/openapi-ts": patch +--- + +**plugin(@hey-api/sdk)**: support `valibot` as response transformer diff --git a/.changeset/heavy-states-cheat.md b/.changeset/heavy-states-cheat.md new file mode 100644 index 0000000000..7fbf328729 --- /dev/null +++ b/.changeset/heavy-states-cheat.md @@ -0,0 +1,5 @@ +--- +"@hey-api/openapi-ts": patch +--- + +**plugin(@hey-api/sdk)**: support `zod` as response transformer diff --git a/packages/openapi-ts-tests/valibot/v1/__snapshots__/3.1.x/type-format/sdk.gen.ts b/packages/openapi-ts-tests/valibot/v1/__snapshots__/3.1.x/type-format/sdk.gen.ts index af4e18178a..c03c1fc987 100644 --- a/packages/openapi-ts-tests/valibot/v1/__snapshots__/3.1.x/type-format/sdk.gen.ts +++ b/packages/openapi-ts-tests/valibot/v1/__snapshots__/3.1.x/type-format/sdk.gen.ts @@ -4,7 +4,6 @@ import * as v from 'valibot'; import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import { postFooResponseTransformer } from './transformers.gen'; import type { PostFooData, PostFooResponses } from './types.gen'; import { vPostFooResponse } from './valibot.gen'; @@ -28,8 +27,7 @@ export const postFoo = (options?: Options< path: v.optional(v.never()), query: v.optional(v.never()) }), data), - responseTransformer: postFooResponseTransformer, - responseValidator: async (data) => await v.parseAsync(vPostFooResponse, data), + responseTransformer: async (data) => await v.parseAsync(vPostFooResponse, data), url: '/foo', ...options }); diff --git a/packages/openapi-ts-tests/valibot/v1/__snapshots__/3.1.x/type-format/types.gen.ts b/packages/openapi-ts-tests/valibot/v1/__snapshots__/3.1.x/type-format/types.gen.ts index 87d807f950..318823eb66 100644 --- a/packages/openapi-ts-tests/valibot/v1/__snapshots__/3.1.x/type-format/types.gen.ts +++ b/packages/openapi-ts-tests/valibot/v1/__snapshots__/3.1.x/type-format/types.gen.ts @@ -10,7 +10,7 @@ export type UserId = TypeID<'user'>; export type Foo = { bar?: number; - foo: bigint; + foo: number; id: UserId; }; diff --git a/packages/openapi-ts-tests/valibot/v1/test/3.1.x.test.ts b/packages/openapi-ts-tests/valibot/v1/test/3.1.x.test.ts index 0a1e962992..69ce20ed7d 100644 --- a/packages/openapi-ts-tests/valibot/v1/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/valibot/v1/test/3.1.x.test.ts @@ -163,7 +163,6 @@ describe(`OpenAPI ${version}`, () => { input: 'type-format.yaml', output: 'type-format', plugins: [ - '@hey-api/transformers', 'valibot', { name: '@hey-api/sdk', diff --git a/web/src/content/docs/docs/openapi/typescript/clients.mdx b/web/src/content/docs/docs/openapi/typescript/clients.mdx index 0d1b281b5b..78a0eca903 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients.mdx @@ -26,16 +26,16 @@ We all send HTTP requests in a slightly different way. Hey API doesn't force you Hey API natively supports the following clients. -- [Fetch API](/openapi-ts/clients/fetch) -- [Angular](/openapi-ts/clients/angular) -- [Axios](/openapi-ts/clients/axios) -- [Ky](/openapi-ts/clients/ky) -- [Next.js](/openapi-ts/clients/next-js) -- [Nuxt](/openapi-ts/clients/nuxt) -- [OFetch](/openapi-ts/clients/ofetch) -- [Effect](/openapi-ts/clients/effect) -- [Got](/openapi-ts/clients/got) - -Don't see your client? [Build your own](/openapi-ts/clients/custom) or let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). +- [Fetch API](/docs/openapi/typescript/clients/fetch) +- [Angular](/docs/openapi/typescript/clients/angular) +- [Axios](/docs/openapi/typescript/clients/axios) +- [Ky](/docs/openapi/typescript/clients/ky) +- [Next.js](/docs/openapi/typescript/clients/next-js) +- [Nuxt](/docs/openapi/typescript/clients/nuxt) +- [OFetch](/docs/openapi/typescript/clients/ofetch) +- [Effect](/docs/openapi/typescript/clients/effect) +- [Got](/docs/openapi/typescript/clients/got) + +Don't see your client? [Build your own](/docs/openapi/typescript/clients/custom) or let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). diff --git a/web/src/content/docs/docs/openapi/typescript/clients/angular/v19.mdx b/web/src/content/docs/docs/openapi/typescript/clients/angular/v19.mdx index 0b78b3a7f0..4403a15b73 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients/angular/v19.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients/angular/v19.mdx @@ -39,7 +39,7 @@ The Angular client for Hey API generates a type-safe client from your OpenAPI sp ## Installation -In your [configuration](/openapi-ts/get-started), add `@hey-api/client-angular` to your plugins and you'll be ready to generate client artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/client-angular` to your plugins and you'll be ready to generate client artifacts. 🎉 @@ -80,7 +80,7 @@ export const appConfig: ApplicationConfig = { The Angular client is built as a thin wrapper on top of Angular, extending its functionality to work with Hey API. If you're already familiar with Angular, configuring your client will feel like working directly with Angular. -When we installed the client above, it created a [`client.gen.ts`](/openapi-ts/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. +When we installed the client above, it created a [`client.gen.ts`](/docs/openapi/typescript/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. ### `setConfig()` @@ -241,7 +241,7 @@ You can use any of the approaches mentioned in [Configuration](#configuration), ## Plugins -You might be also interested in the [Angular](/openapi-ts/plugins/angular/v19) plugin. +You might be also interested in the [Angular](/docs/openapi/typescript/plugins/angular/v19) plugin. ## API diff --git a/web/src/content/docs/docs/openapi/typescript/clients/angular/v20.mdx b/web/src/content/docs/docs/openapi/typescript/clients/angular/v20.mdx index c174a6d474..0d9bd74855 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients/angular/v20.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients/angular/v20.mdx @@ -41,7 +41,7 @@ The Angular client for Hey API generates a type-safe client from your OpenAPI sp ## Installation -In your [configuration](/openapi-ts/get-started), add `@hey-api/client-angular` to your plugins and you'll be ready to generate client artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/client-angular` to your plugins and you'll be ready to generate client artifacts. 🎉 @@ -82,7 +82,7 @@ export const appConfig: ApplicationConfig = { The Angular client is built as a thin wrapper on top of Angular, extending its functionality to work with Hey API. If you're already familiar with Angular, configuring your client will feel like working directly with Angular. -When we installed the client above, it created a [`client.gen.ts`](/openapi-ts/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. +When we installed the client above, it created a [`client.gen.ts`](/docs/openapi/typescript/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. ### `setConfig()` @@ -243,7 +243,7 @@ You can use any of the approaches mentioned in [Configuration](#configuration), ## Plugins -You might be also interested in the [Angular](/openapi-ts/plugins/angular) plugin. +You might be also interested in the [Angular](/docs/openapi/typescript/plugins/angular) plugin. ## API diff --git a/web/src/content/docs/docs/openapi/typescript/clients/axios.mdx b/web/src/content/docs/docs/openapi/typescript/clients/axios.mdx index 541ccbbb4a..9adb901bec 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients/axios.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients/axios.mdx @@ -45,7 +45,7 @@ The Axios client for Hey API generates a type-safe client from your OpenAPI spec ## Installation -In your [configuration](/openapi-ts/get-started), add `@hey-api/client-axios` to your plugins and you'll be ready to generate client artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/client-axios` to your plugins and you'll be ready to generate client artifacts. 🎉 @@ -71,7 +71,7 @@ npx @hey-api/openapi-ts \ The Axios client is built as a thin wrapper on top of Axios, extending its functionality to work with Hey API. If you're already familiar with Axios, configuring your client will feel like working directly with Axios. -When we installed the client above, it created a [`client.gen.ts`](/openapi-ts/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. +When we installed the client above, it created a [`client.gen.ts`](/docs/openapi/typescript/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. ### `setConfig()` diff --git a/web/src/content/docs/docs/openapi/typescript/clients/fetch.mdx b/web/src/content/docs/docs/openapi/typescript/clients/fetch.mdx index 34454921e4..7f397a722a 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients/fetch.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients/fetch.mdx @@ -42,7 +42,7 @@ The Fetch API client for Hey API generates a type-safe client from your OpenAPI ## Installation -In your [configuration](/openapi-ts/get-started), add `@hey-api/client-fetch` to your plugins and you'll be ready to generate client artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/client-fetch` to your plugins and you'll be ready to generate client artifacts. 🎉 @@ -72,7 +72,7 @@ This step is optional because Fetch is the default client. The Fetch client is built as a thin wrapper on top of Fetch API, extending its functionality to work with Hey API. If you're already familiar with Fetch, configuring your client will feel like working directly with Fetch API. -When we installed the client above, it created a [`client.gen.ts`](/openapi-ts/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. +When we installed the client above, it created a [`client.gen.ts`](/docs/openapi/typescript/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. ### `setConfig()` diff --git a/web/src/content/docs/docs/openapi/typescript/clients/ky.mdx b/web/src/content/docs/docs/openapi/typescript/clients/ky.mdx index 0544b8523e..bc71902234 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients/ky.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients/ky.mdx @@ -36,7 +36,7 @@ The Ky client for Hey API generates a type-safe client from your OpenAPI spec, f ## Installation -In your [configuration](/openapi-ts/get-started), add `@hey-api/client-ky` to your plugins and you'll be ready to generate client artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/client-ky` to your plugins and you'll be ready to generate client artifacts. 🎉 @@ -62,7 +62,7 @@ npx @hey-api/openapi-ts \ The Ky client is built as a thin wrapper on top of Ky, extending its functionality to work with Hey API. If you're already familiar with Ky, configuring your client will feel like working directly with Ky. -When we installed the client above, it created a [`client.gen.ts`](/openapi-ts/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. +When we installed the client above, it created a [`client.gen.ts`](/docs/openapi/typescript/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. ### `setConfig()` diff --git a/web/src/content/docs/docs/openapi/typescript/clients/next-js.mdx b/web/src/content/docs/docs/openapi/typescript/clients/next-js.mdx index 945078f370..f562696839 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients/next-js.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients/next-js.mdx @@ -28,7 +28,7 @@ The Next.js client for Hey API generates a type-safe client from your OpenAPI sp ## Installation -In your [configuration](/openapi-ts/get-started), add `@hey-api/client-next` to your plugins and you'll be ready to generate client artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/client-next` to your plugins and you'll be ready to generate client artifacts. 🎉 @@ -54,7 +54,7 @@ npx @hey-api/openapi-ts \ The Next.js client is built as a thin wrapper on top of [fetch](https://nextjs.org/docs/app/api-reference/functions/fetch), extending its functionality to work with Hey API. If you're already familiar with Fetch, configuring your client will feel like working directly with Fetch API. -When we installed the client above, it created a [`client.gen.ts`](/openapi-ts/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. +When we installed the client above, it created a [`client.gen.ts`](/docs/openapi/typescript/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. ### Runtime API diff --git a/web/src/content/docs/docs/openapi/typescript/clients/nuxt.mdx b/web/src/content/docs/docs/openapi/typescript/clients/nuxt.mdx index 1e2b13a087..0c3663f05b 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients/nuxt.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients/nuxt.mdx @@ -39,7 +39,7 @@ Start by adding `@hey-api/nuxt` to your dependencies. -In your [configuration](/openapi-ts/get-started), add `@hey-api/client-nuxt` to your plugins and you'll be ready to generate client artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/client-nuxt` to your plugins and you'll be ready to generate client artifacts. 🎉 @@ -69,7 +69,7 @@ If you add `@hey-api/nuxt` to your Nuxt modules, this step is not needed. The Nuxt client is built as a thin wrapper on top of Nuxt, extending its functionality to work with Hey API. If you're already familiar with Nuxt, configuring your client will feel like working directly with Nuxt. -When we installed the client above, it created a [`client.gen.ts`](/openapi-ts/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. +When we installed the client above, it created a [`client.gen.ts`](/docs/openapi/typescript/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. ### `setConfig()` diff --git a/web/src/content/docs/docs/openapi/typescript/clients/ofetch.mdx b/web/src/content/docs/docs/openapi/typescript/clients/ofetch.mdx index 052e374c61..4ff322c31f 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients/ofetch.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients/ofetch.mdx @@ -34,7 +34,7 @@ The `ofetch` client for Hey API generates a type-safe client from your OpenAPI s ## Installation -In your [configuration](/openapi-ts/get-started), add `@hey-api/client-ofetch` to your plugins and you'll be ready to generate client artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/client-ofetch` to your plugins and you'll be ready to generate client artifacts. 🎉 @@ -60,7 +60,7 @@ npx @hey-api/openapi-ts \ The `ofetch` client is built as a thin wrapper on top of `ofetch`, extending its functionality to work with Hey API. If you're already familiar with `ofetch`, configuring your client will feel like working directly with `ofetch`. -When we installed the client above, it created a [`client.gen.ts`](/openapi-ts/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. +When we installed the client above, it created a [`client.gen.ts`](/docs/openapi/typescript/output#client) file. You will most likely want to configure the exported `client` instance. There are two ways to do that. ### `setConfig()` diff --git a/web/src/content/docs/docs/openapi/typescript/community/contributing.mdx b/web/src/content/docs/docs/openapi/typescript/community/contributing.mdx index b01742d407..5c61c5420b 100644 --- a/web/src/content/docs/docs/openapi/typescript/community/contributing.mdx +++ b/web/src/content/docs/docs/openapi/typescript/community/contributing.mdx @@ -21,7 +21,7 @@ New to open source? Take a look at the [Open Source](https://opensource.guide/) There are many ways to contribute to Hey API. Most of them don't involve writing any code! -- **Read the documentation**. Start with the [Get Started](/openapi-ts/get-started) guide. If you find anything broken or confusing, you can suggest improvements by clicking "Edit" at the bottom of any page. +- **Read the documentation**. Start with the [Get Started](/docs/openapi/typescript/get-started) guide. If you find anything broken or confusing, you can suggest improvements by clicking "Edit" at the bottom of any page. - **Browse open issues**. Help others by providing workarounds, asking for clarification, triaging, or suggesting labels on [open issues](https://github.com/hey-api/openapi-ts/issues). If you see something you would like to work on, consider opening a pull request. @@ -37,6 +37,6 @@ These are some of the best ways not only to contribute to Hey API, but also to l ## Pull Requests -Ready to write some code? We have dedicated guides to help you [build](/openapi-ts/community/contributing/building), [develop](/openapi-ts/community/contributing/developing), and [test](/openapi-ts/community/contributing/testing) your feature before it's released. +Ready to write some code? We have dedicated guides to help you [build](/docs/openapi/typescript/community/contributing/building), [develop](/docs/openapi/typescript/community/contributing/developing), and [test](/docs/openapi/typescript/community/contributing/testing) your feature before it's released. We are excited to see what you'll contribute! diff --git a/web/src/content/docs/docs/openapi/typescript/community/spotlight.mdx b/web/src/content/docs/docs/openapi/typescript/community/spotlight.mdx index 132352cf39..11297423de 100644 --- a/web/src/content/docs/docs/openapi/typescript/community/spotlight.mdx +++ b/web/src/content/docs/docs/openapi/typescript/community/spotlight.mdx @@ -9,7 +9,7 @@ import TeamMembers from '@/components/TeamMembers.astro'; # Spotlight -Meet the people behind Hey API. To join this list, please refer to the [contributing](/openapi-ts/community/contributing) guide. +Meet the people behind Hey API. To join this list, please refer to the [contributing](/docs/openapi/typescript/community/contributing) guide. ## Core Team diff --git a/web/src/content/docs/docs/openapi/typescript/configuration.mdx b/web/src/content/docs/docs/openapi/typescript/configuration.mdx index 9265e4f507..fe9e17bd92 100644 --- a/web/src/content/docs/docs/openapi/typescript/configuration.mdx +++ b/web/src/content/docs/docs/openapi/typescript/configuration.mdx @@ -51,9 +51,9 @@ Alternatively, you can use `openapi-ts.config.js` and configure the export state You must provide an input so we can load your OpenAPI specification. -The input can be a string path, URL, [API registry](/openapi-ts/configuration/input#api-registry) shorthand, an object containing any of these, or an object representing an OpenAPI specification. Hey API supports all valid OpenAPI versions and file formats. +The input can be a string path, URL, [API registry](/docs/openapi/typescript/configuration/input#api-registry) shorthand, an object containing any of these, or an object representing an OpenAPI specification. Hey API supports all valid OpenAPI versions and file formats. -You can learn more on the [Input](/openapi-ts/configuration/input) page. +You can learn more on the [Input](/docs/openapi/typescript/configuration/input) page. @@ -65,7 +65,7 @@ If you use an HTTPS URL with a self-signed certificate in development, you will You must set the output so we know where to generate your files. It can be a path to the destination folder or an object containing the destination folder path and optional settings. -You can learn more on the [Output](/openapi-ts/configuration/output) page. +You can learn more on the [Output](/docs/openapi/typescript/configuration/output) page. @@ -77,13 +77,13 @@ You should treat the output folder as a dependency. Do not directly modify its c We parse your input before making it available to plugins. Configuring the parser is optional, but it provides an ideal opportunity to modify or validate your input as needed. -You can learn more on the [Parser](/openapi-ts/configuration/parser) page. +You can learn more on the [Parser](/docs/openapi/typescript/configuration/parser) page. ## Plugins Plugins are responsible for generating artifacts from your input. By default, Hey API will generate TypeScript interfaces and SDK from your OpenAPI specification. You can add, remove, or customize any of the plugins. In fact, we highly encourage you to do so! -You can learn more on the [Output](/openapi-ts/output) page. +You can learn more on the [Output](/docs/openapi/typescript/output) page. ## Advanced diff --git a/web/src/content/docs/docs/openapi/typescript/configuration/input.mdx b/web/src/content/docs/docs/openapi/typescript/configuration/input.mdx index 51fd63ddcf..74075b14e8 100644 --- a/web/src/content/docs/docs/openapi/typescript/configuration/input.mdx +++ b/web/src/content/docs/docs/openapi/typescript/configuration/input.mdx @@ -17,7 +17,7 @@ The input can be a string path, URL, [API registry](#api-registry), an object co -You can learn more about complex use cases in the [Advanced](/openapi-ts/configuration#advanced) section. +You can learn more about complex use cases in the [Advanced](/docs/openapi/typescript/configuration#advanced) section. :::tip If you use an HTTPS URL with a self-signed certificate in development, you will need to set [`NODE_TLS_REJECT_UNAUTHORIZED=0`](https://github.com/hey-api/openapi-ts/issues/276#issuecomment-2043143501) in your environment. @@ -46,7 +46,7 @@ You can store your specifications in an API registry to serve as a single source ### Hey API -You can learn more about [Hey API Platform](https://app.heyapi.dev) on the [Integrations](/openapi-ts/integrations) page. +You can learn more about [Hey API Platform](https://app.heyapi.dev) on the [Integrations](/docs/openapi/typescript/integrations) page. ```js {2} title="openapi-ts.config.ts" export default { diff --git a/web/src/content/docs/docs/openapi/typescript/configuration/output.mdx b/web/src/content/docs/docs/openapi/typescript/configuration/output.mdx index 5e0c80cdd6..f4088403ff 100644 --- a/web/src/content/docs/docs/openapi/typescript/configuration/output.mdx +++ b/web/src/content/docs/docs/openapi/typescript/configuration/output.mdx @@ -17,7 +17,7 @@ Output can be a path to the destination folder or an object containing the desti -You can learn more about complex use cases in the [Advanced](/openapi-ts/configuration#advanced) section. +You can learn more about complex use cases in the [Advanced](/docs/openapi/typescript/configuration#advanced) section. ## File diff --git a/web/src/content/docs/docs/openapi/typescript/configuration/parser.mdx b/web/src/content/docs/docs/openapi/typescript/configuration/parser.mdx index 4266d92821..aa36072f03 100644 --- a/web/src/content/docs/docs/openapi/typescript/configuration/parser.mdx +++ b/web/src/content/docs/docs/openapi/typescript/configuration/parser.mdx @@ -586,7 +586,7 @@ We always use the first hook that returns a value. If a hook returns no value, w

Operations

-Each operation has a list of classifiers that can include `query`, `mutation`, both, or none. Plugins may use these values to decide whether to generate specific output. For example, you usually don't want to generate [TanStack Query options](/openapi-ts/plugins/tanstack-query#queries) for PATCH operations. +Each operation has a list of classifiers that can include `query`, `mutation`, both, or none. Plugins may use these values to decide whether to generate specific output. For example, you usually don't want to generate [TanStack Query options](/docs/openapi/typescript/plugins/tanstack-query#queries) for PATCH operations.

Query operations

@@ -598,7 +598,7 @@ By default, DELETE, PATCH, POST, and PUT operations are classified as `mutation` #### Example: POST search query -Imagine your API has a POST `/search` endpoint that accepts a large payload. By default, it's classified as a `mutation`, but in practice it behaves like a `query`, and your [state management](/openapi-ts/state-management) plugin should generate query hooks. +Imagine your API has a POST `/search` endpoint that accepts a large payload. By default, it's classified as a `mutation`, but in practice it behaves like a `query`, and your [state management](/docs/openapi/typescript/state-management) plugin should generate query hooks. You can achieve this by classifying the operation as `query` in a matcher. diff --git a/web/src/content/docs/docs/openapi/typescript/configuration/vite.mdx b/web/src/content/docs/docs/openapi/typescript/configuration/vite.mdx index 08c74974a1..6b3bc8344c 100644 --- a/web/src/content/docs/docs/openapi/typescript/configuration/vite.mdx +++ b/web/src/content/docs/docs/openapi/typescript/configuration/vite.mdx @@ -19,7 +19,7 @@ The Vite plugin integrates `@hey-api/openapi-ts` into the Vite build pipeline, r ## Features - runs automatically as part of your Vite build -- reads your existing [configuration](/openapi-ts/get-started) (or accepts inline config) +- reads your existing [configuration](/docs/openapi/typescript/get-started) (or accepts inline config) - supports Vite 5, 6, 7, and 8 ## Installation @@ -41,7 +41,7 @@ export default defineConfig({ }); ``` -The plugin will automatically pick up your [configuration](/openapi-ts/configuration) file. You can also pass options inline using the `config` option: +The plugin will automatically pick up your [configuration](/docs/openapi/typescript/configuration) file. You can also pass options inline using the `config` option: ```js title="vite.config.ts" import { heyApiPlugin } from '@hey-api/vite-plugin'; diff --git a/web/src/content/docs/docs/openapi/typescript/core.mdx b/web/src/content/docs/docs/openapi/typescript/core.mdx index 13eb5ab9e9..c4abfddd3c 100644 --- a/web/src/content/docs/docs/openapi/typescript/core.mdx +++ b/web/src/content/docs/docs/openapi/typescript/core.mdx @@ -15,10 +15,10 @@ Apart from being responsible for the default output, core plugins are the founda Hey API provides the following core plugins. -- [TypeScript](/openapi-ts/plugins/typescript) -- [SDK](/openapi-ts/plugins/sdk) -- [Transformers](/openapi-ts/plugins/transformers) -- [Schemas](/openapi-ts/plugins/schemas) +- [TypeScript](/docs/openapi/typescript/plugins/typescript) +- [SDK](/docs/openapi/typescript/plugins/sdk) +- [Transformers](/docs/openapi/typescript/plugins/transformers) +- [Schemas](/docs/openapi/typescript/plugins/schemas) Need another core plugin? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). diff --git a/web/src/content/docs/docs/openapi/typescript/get-started.mdx b/web/src/content/docs/docs/openapi/typescript/get-started.mdx index dfcd609327..2e889ee126 100644 --- a/web/src/content/docs/docs/openapi/typescript/get-started.mdx +++ b/web/src/content/docs/docs/openapi/typescript/get-started.mdx @@ -39,7 +39,7 @@ Used by companies like Vercel, OpenCode, and PayPal. - HTTP clients for Fetch API, Angular, Axios, Next.js, Nuxt, and more - 20+ plugins to reduce third-party boilerplate - highly customizable via plugins -- [sync with Hey API Registry](/openapi-ts/integrations) for spec management +- [sync with Hey API Registry](/docs/openapi/typescript/integrations) for spec management ## Quick Start @@ -49,7 +49,7 @@ The fastest way to use `@hey-api/openapi-ts` is via npx npx @hey-api/openapi-ts -i hey-api/backend -o src/client ``` -Congratulations on creating your first client! 🎉 You can learn more about the generated files on the [Output](/openapi-ts/output) page. +Congratulations on creating your first client! 🎉 You can learn more about the generated files on the [Output](/docs/openapi/typescript/output) page. ## Installation @@ -61,7 +61,7 @@ You can download `@hey-api/openapi-ts` from npm using your favorite package mana This package is in [initial development](https://semver.org/#spec-item-4). Please pin an exact version so you can safely upgrade when you're ready. -We publish [migration notes](/openapi-ts/migrating) for every breaking release. You might not be impacted by a breaking change if you don't use the affected features. +We publish [migration notes](/docs/openapi/typescript/migrating) for every breaking release. You might not be impacted by a breaking change if you don't use the affected features. ## Usage @@ -75,7 +75,7 @@ Most people run `@hey-api/openapi-ts` via CLI. To do that, add a script to your } ``` -The above script can be executed by running `npm run openapi-ts` or equivalent command in other package managers. Next, we will create a [configuration](/openapi-ts/configuration) file and move our options from Quick Start to it. +The above script can be executed by running `npm run openapi-ts` or equivalent command in other package managers. Next, we will create a [configuration](/docs/openapi/typescript/configuration) file and move our options from Quick Start to it. ### Node.js @@ -114,10 +114,10 @@ export default defineConfig({ }); ``` -See the [Vite](/openapi-ts/configuration/vite) page for full configuration options. +See the [Vite](/docs/openapi/typescript/configuration/vite) page for full configuration options. ### Configuration -It's a good practice to extract your configuration into a separate file. Learn how to do that and discover available options on the [Configuration](/openapi-ts/configuration) page. +It's a good practice to extract your configuration into a separate file. Learn how to do that and discover available options on the [Configuration](/docs/openapi/typescript/configuration) page. diff --git a/web/src/content/docs/docs/openapi/typescript/migrating.mdx b/web/src/content/docs/docs/openapi/typescript/migrating.mdx index 82fb574340..7f8edbc301 100644 --- a/web/src/content/docs/docs/openapi/typescript/migrating.mdx +++ b/web/src/content/docs/docs/openapi/typescript/migrating.mdx @@ -134,13 +134,13 @@ If your environment cannot use ESM, pin to a previous version. ### Resolvers API -The [Resolvers API](/openapi-ts/plugins/concepts/resolvers) has been simplified and expanded to provide a more consistent behavior across plugins. You can view a few common examples on the [Resolvers](/openapi-ts/plugins/concepts/resolvers) page. +The [Resolvers API](/docs/openapi/typescript/plugins/concepts/resolvers) has been simplified and expanded to provide a more consistent behavior across plugins. You can view a few common examples on the [Resolvers](/docs/openapi/typescript/plugins/concepts/resolvers) page. ### Structure API -The [SDK plugin](/openapi-ts/plugins/sdk) and [Angular plugin](/openapi-ts/plugins/angular) now implement the Structure API, enabling more complex structures and fixing several known issues. +The [SDK plugin](/docs/openapi/typescript/plugins/sdk) and [Angular plugin](/docs/openapi/typescript/plugins/angular) now implement the Structure API, enabling more complex structures and fixing several known issues. -Some Structure APIs are incompatible with the previous configuration, most notably the `methodNameBuilder` function, which accepted the operation object as an argument. You can read the [SDK Output](/openapi-ts/plugins/sdk#output) section to familiarize yourself with the Structure API. +Some Structure APIs are incompatible with the previous configuration, most notably the `methodNameBuilder` function, which accepted the operation object as an argument. You can read the [SDK Output](/docs/openapi/typescript/plugins/sdk#output) section to familiarize yourself with the Structure API. Please [open an issue](https://github.com/hey-api/openapi-ts/issues) if you're unable to migrate your configuration to the new syntax. @@ -255,7 +255,7 @@ useQuery(() => ({ This release improves the Symbol API, which adds the capability to place symbols in arbitrary files. We preserved the previous output structure for all plugins except Angular. -You can preserve the previous Angular output by writing your own [placement function](/openapi-ts/configuration/parser#hooks-symbols). +You can preserve the previous Angular output by writing your own [placement function](/docs/openapi/typescript/configuration/parser#hooks-symbols). ### TypeScript renderer @@ -271,7 +271,7 @@ Due to the Symbol API release, this option has been removed from the Plugin API. This release adds the Symbol API, which significantly reduces the risk of naming collisions. While the generated output should only include formatting changes, this feature introduces breaking changes to the Plugin API that affect custom plugins. -We will update the [custom plugin guide](/openapi-ts/plugins/custom) once the Plugin API becomes more stable. +We will update the [custom plugin guide](/docs/openapi/typescript/plugins/custom) once the Plugin API becomes more stable. ### Removed `groupByTag` Pinia Colada option @@ -281,7 +281,7 @@ This option has been removed to provide a more consistent API across plugins. We ### Hooks API -This release adds the [Hooks API](/openapi-ts/configuration/parser#hooks), giving you granular control over which operations generate queries and mutations. As a result, we tightened the previous behavior and POST operations no longer generate queries by default. To preserve the old behavior, add a custom matcher. +This release adds the [Hooks API](/docs/openapi/typescript/configuration/parser#hooks), giving you granular control over which operations generate queries and mutations. As a result, we tightened the previous behavior and POST operations no longer generate queries by default. To preserve the old behavior, add a custom matcher. ```js {7} title="openapi-ts.config.ts" export default { @@ -373,7 +373,7 @@ Previously, `@hey-api/typescript` would generate correct types, but the validato Since neither option was ideal, this release adds a dedicated place for `parser` options. Parser is responsible for preparing the input so plugins can generate more accurate output with less effort. -You can learn more about configuring parser on the [Parser](/openapi-ts/configuration/parser) page. +You can learn more about configuring parser on the [Parser](/docs/openapi/typescript/configuration/parser) page. ### Moved `input` options @@ -430,7 +430,7 @@ export default { ### Updated Plugin API -Please refer to the [custom plugin](/openapi-ts/plugins/custom) tutorial for the latest guide. +Please refer to the [custom plugin](/docs/openapi/typescript/plugins/custom) tutorial for the latest guide. ## v0.76.0 @@ -1638,7 +1638,7 @@ This command is now called `openapi-ts`. ### Removed `indent` -This config option has been removed. Use a [code formatter](/openapi-ts/configuration/output#post-process) to modify the generated files code style according to your preferences. +This config option has been removed. Use a [code formatter](/docs/openapi/typescript/configuration/output#post-process) to modify the generated files code style according to your preferences. ## v0.27.24 diff --git a/web/src/content/docs/docs/openapi/typescript/mocks.mdx b/web/src/content/docs/docs/openapi/typescript/mocks.mdx index 38d642122b..e01ca99f82 100644 --- a/web/src/content/docs/docs/openapi/typescript/mocks.mdx +++ b/web/src/content/docs/docs/openapi/typescript/mocks.mdx @@ -16,12 +16,12 @@ Realistic mock data is an important component of every robust development proces Hey API natively supports the following mocking frameworks. -- [Chance](/openapi-ts/plugins/chance) -- [Faker](/openapi-ts/plugins/faker) -- [Falso](/openapi-ts/plugins/falso) -- [MSW](/openapi-ts/plugins/msw) -- [Nock](/openapi-ts/plugins/nock) -- [Supertest](/openapi-ts/plugins/supertest) +- [Chance](/docs/openapi/typescript/plugins/chance) +- [Faker](/docs/openapi/typescript/plugins/faker) +- [Falso](/docs/openapi/typescript/plugins/falso) +- [MSW](/docs/openapi/typescript/plugins/msw) +- [Nock](/docs/openapi/typescript/plugins/nock) +- [Supertest](/docs/openapi/typescript/plugins/supertest) Don't see your framework? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). diff --git a/web/src/content/docs/docs/openapi/typescript/output.mdx b/web/src/content/docs/docs/openapi/typescript/output.mdx index 0a0346623e..0c6bfe039b 100644 --- a/web/src/content/docs/docs/openapi/typescript/output.mdx +++ b/web/src/content/docs/docs/openapi/typescript/output.mdx @@ -35,7 +35,7 @@ Let's go through each file in the `src/client` folder and explain what it looks ## Client -`client.gen.ts` is generated by [client plugins](/openapi-ts/clients). If you choose to generate SDKs (enabled by default), we use the Fetch client unless specified otherwise. +`client.gen.ts` is generated by [client plugins](/docs/openapi/typescript/clients). If you choose to generate SDKs (enabled by default), we use the Fetch client unless specified otherwise. ```ts title="client.gen.ts" import { createClient, createConfig } from './client'; @@ -51,11 +51,11 @@ Client plugins provide their bundles inside `client` and `core` folders. The con ## TypeScript -You can learn more on the [TypeScript](/openapi-ts/plugins/typescript) page. +You can learn more on the [TypeScript](/docs/openapi/typescript/plugins/typescript) page. ## SDK -You can learn more on the [SDK](/openapi-ts/plugins/sdk) page. +You can learn more on the [SDK](/docs/openapi/typescript/plugins/sdk) page. ## Entry File @@ -90,7 +90,7 @@ export default { ### Re-export artifacts -You can choose which artifacts should be re-exported from the entry file using the `includeInEntry` option on any plugin. For example, we can re-export all [Zod](/openapi-ts/plugins/zod) plugin artifacts by setting `includeInEntry` to `true`: +You can choose which artifacts should be re-exported from the entry file using the `includeInEntry` option on any plugin. For example, we can re-export all [Zod](/docs/openapi/typescript/plugins/zod) plugin artifacts by setting `includeInEntry` to `true`: diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/angular/v19.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/angular/v19.mdx index 367eb1ba22..bb7afbbdad 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/angular/v19.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/angular/v19.mdx @@ -35,7 +35,7 @@ The Angular plugin for Hey API generates HTTP requests and resources from your O ## Installation -In your [configuration](/openapi-ts/get-started), add `@angular/common` to your plugins and you'll be ready to generate Angular artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@angular/common` to your plugins and you'll be ready to generate Angular artifacts. 🎉 ```js {6} title="openapi-ts.config.ts" export default { diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/angular/v20.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/angular/v20.mdx index cf052555af..3db1446634 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/angular/v20.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/angular/v20.mdx @@ -37,7 +37,7 @@ The Angular plugin for Hey API generates HTTP requests and resources from your O ## Installation -In your [configuration](/openapi-ts/get-started), add `@angular/common` to your plugins and you'll be ready to generate Angular artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@angular/common` to your plugins and you'll be ready to generate Angular artifacts. 🎉 ```js {6} title="openapi-ts.config.ts" export default { diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/concepts/resolvers.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/concepts/resolvers.mdx index d2470995c7..056ac9bed5 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/concepts/resolvers.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/concepts/resolvers.mdx @@ -9,7 +9,7 @@ import { Tabs, TabItem } from '@astrojs/starlight/components'; Sometimes the default plugin behavior isn't what you need or expect. Resolvers let you patch plugins in a safe and performant way, without forking or reimplementing core logic. -Currently available for [TypeScript](/openapi-ts/plugins/typescript), [Valibot](/openapi-ts/plugins/valibot), and [Zod](/openapi-ts/plugins/zod). +Currently available for [TypeScript](/docs/openapi/typescript/plugins/typescript), [Valibot](/docs/openapi/typescript/plugins/valibot), and [Zod](/docs/openapi/typescript/plugins/zod). ## Examples diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/custom.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/custom.mdx index 18381a00ec..34c22bde69 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/custom.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/custom.mdx @@ -13,7 +13,7 @@ Plugin API is in development. The interface might change before it becomes stabl ::: :::caution -This page is out of date as of [v0.83.0](/openapi-ts/migrating#v0-83-0). If you have an existing custom plugin, we recommend waiting for a more stable Plugin API to avoid multiple plugin rewrites. +This page is out of date as of [v0.83.0](/docs/openapi/typescript/migrating#v0-83-0). If you have an existing custom plugin, we recommend waiting for a more stable Plugin API to avoid multiple plugin rewrites. ::: You may need to write your own plugin if the available plugins do not suit your needs or you're working on a proprietary use case. This can be easily achieved using the Plugin API. But don't take our word for it – all Hey API plugins are written this way! @@ -136,7 +136,7 @@ You can also define an optional `handlerLegacy` function in `config.ts`. This me ## Usage -Once we're satisfied with our plugin, we can register it in the [configuration](/openapi-ts/configuration) file. +Once we're satisfied with our plugin, we can register it in the [configuration](/docs/openapi/typescript/configuration) file. ```js title="openapi-ts.config.ts" import { defineConfig } from 'path/to/my-plugin'; diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/fastify.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/fastify.mdx index ce604e66da..92580beab1 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/fastify.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/fastify.mdx @@ -36,7 +36,7 @@ The Fastify plugin for Hey API generates route handlers from your OpenAPI spec, ## Installation -In your [configuration](/openapi-ts/get-started), add `fastify` to your plugins and you'll be ready to generate Fastify artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `fastify` to your plugins and you'll be ready to generate Fastify artifacts. 🎉 ```js {6} title="openapi-ts.config.ts" export default { diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/nest.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/nest.mdx index 1e8fb40792..53074e0659 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/nest.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/nest.mdx @@ -36,7 +36,7 @@ The NestJS plugin for Hey API generates type-safe controller method signatures f ## Installation -In your [configuration](/openapi-ts/get-started), add `nestjs` to your plugins and you'll be ready to generate NestJS artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `nestjs` to your plugins and you'll be ready to generate NestJS artifacts. 🎉 ```js {6} title="openapi-ts.config.ts" export default { diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/orpc.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/orpc.mdx index 216ba65843..317bc539f6 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/orpc.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/orpc.mdx @@ -36,7 +36,7 @@ The oRPC plugin for Hey API generates contracts from your OpenAPI spec, fully co ## Installation -In your [configuration](/openapi-ts/get-started), add `orpc` to your plugins and you'll be ready to generate oRPC artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `orpc` to your plugins and you'll be ready to generate oRPC artifacts. 🎉 ```js {6} title="openapi-ts.config.ts" export default { @@ -89,7 +89,7 @@ export default { ### Validators -To enable schema validation, set `validator` to `zod` or one of the available [validator plugins](/openapi-ts/validators). This will implicitly add the selected plugin with default values. +To enable schema validation, set `validator` to `zod` or one of the available [validator plugins](/docs/openapi/typescript/validators). This will implicitly add the selected plugin with default values. For a more granular approach, manually add a validator plugin and set `validator` to the plugin name or `true` to automatically select a compatible plugin. Until you customize the validator plugin, both approaches will produce the same default output. @@ -174,7 +174,7 @@ export default { -Learn more about available validators on the [Validators](/openapi-ts/validators) page. +Learn more about available validators on the [Validators](/docs/openapi/typescript/validators) page. ## API diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/pinia-colada.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/pinia-colada.mdx index f6c4b500e6..acb727d0a1 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/pinia-colada.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/pinia-colada.mdx @@ -34,7 +34,7 @@ The Pinia Colada plugin for Hey API generates functions and query keys from your ## Installation -In your [configuration](/openapi-ts/get-started), add `@pinia/colada` to your plugins and you'll be ready to generate Pinia Colada artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@pinia/colada` to your plugins and you'll be ready to generate Pinia Colada artifacts. 🎉 ```js {6} title="openapi-ts.config.ts" export default { @@ -48,9 +48,9 @@ export default { ``` :::tip -When using this plugin in a Nuxt app, prefer the [ofetch client](/openapi-ts/clients/ofetch) for universal compatibility. +When using this plugin in a Nuxt app, prefer the [ofetch client](/docs/openapi/typescript/clients/ofetch) for universal compatibility. -The [nuxt client](/openapi-ts/clients/nuxt) is tailored for working directly with Nuxt composables (`$fetch` / `useFetch` / `useAsyncData`) and is not intended as a universal HTTP client for libraries like `@pinia/colada`. +The [nuxt client](/docs/openapi/typescript/clients/nuxt) is tailored for working directly with Nuxt composables (`$fetch` / `useFetch` / `useAsyncData`) and is not intended as a universal HTTP client for libraries like `@pinia/colada`. ::: ## Output @@ -59,7 +59,7 @@ The Pinia Colada plugin will generate the following artifacts, depending on the ## Queries -Queries are generated from [query operations](/openapi-ts/configuration/parser#hooks-query-operations). The generated query functions follow the naming convention of SDK functions and by default append `Query`, e.g., `getPetByIdQuery()`. +Queries are generated from [query operations](/docs/openapi/typescript/configuration/parser#hooks-query-operations). The generated query functions follow the naming convention of SDK functions and by default append `Query`, e.g., `getPetByIdQuery()`. @@ -227,7 +227,7 @@ You can customize the naming and casing pattern for `queryKeys` functions using ## Mutations -Mutations are generated from [mutation operations](/openapi-ts/configuration/parser#hooks-mutation-operations). The generated mutation functions follow the naming convention of SDK functions and by default append `Mutation`, e.g., `addPetMutation()`. +Mutations are generated from [mutation operations](/docs/openapi/typescript/configuration/parser#hooks-mutation-operations). The generated mutation functions follow the naming convention of SDK functions and by default append `Mutation`, e.g., `addPetMutation()`. diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/sdk.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/sdk.mdx index d735c6d713..6a1f7fb0d1 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/sdk.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/sdk.mdx @@ -26,7 +26,7 @@ It exposes typed functions or methods for each operation, with built-in auth han ## Installation -In your [configuration](/openapi-ts/get-started), add `@hey-api/sdk` to your plugins and you'll be ready to generate SDK artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/sdk` to your plugins and you'll be ready to generate SDK artifacts. 🎉 ```js {6} title="openapi-ts.config.ts" export default { @@ -241,7 +241,7 @@ The SDK plugin currently supports only the `bearer` and `basic` auth schemes. [O ## Validators -Validating data at runtime comes with a performance cost, which is why it's not enabled by default. To enable validation, set `validator` to `zod` or one of the available [validator plugins](/openapi-ts/validators). This will implicitly add the selected plugin with default values. +Validating data at runtime comes with a performance cost, which is why it's not enabled by default. To enable validation, set `validator` to `zod` or one of the available [validator plugins](/docs/openapi/typescript/validators). This will implicitly add the selected plugin with default values. For a more granular approach, manually add a validator plugin and set `validator` to the plugin name or `true` to automatically select a compatible plugin. Until you customize the validator plugin, both approaches will produce the same default output. @@ -320,7 +320,11 @@ export default { -Learn more about available validators on the [Validators](/openapi-ts/validators) page. +Learn more about available validators on the [Validators](/docs/openapi/typescript/validators) page. + +## Transformers + +TODO ## Code Examples @@ -557,7 +561,7 @@ export default { ### Display -Enabling examples does not produce visible output on its own. Examples are written into the source specification and can be consumed by documentation tools such as [Scalar](https://kutt.to/skQUVd). To persist that specification, enable [Source](/openapi-ts/configuration/output#source) generation. +Enabling examples does not produce visible output on its own. Examples are written into the source specification and can be consumed by documentation tools such as [Scalar](https://kutt.to/skQUVd). To persist that specification, enable [Source](/docs/openapi/typescript/configuration/output#source) generation. ## API diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/tanstack-query.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/tanstack-query.mdx index 941844e19d..99a23523a1 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/tanstack-query.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/tanstack-query.mdx @@ -41,7 +41,7 @@ The TanStack Query plugin for Hey API generates functions and query keys from yo ## Installation -In your [configuration](/openapi-ts/get-started), add TanStack Query to your plugins and you'll be ready to generate TanStack Query artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add TanStack Query to your plugins and you'll be ready to generate TanStack Query artifacts. 🎉 @@ -124,7 +124,7 @@ The TanStack Query plugin will generate the following artifacts, depending on th ## Queries -Queries are generated from [query operations](/openapi-ts/configuration/parser#hooks-query-operations). The generated query functions follow the naming convention of SDK functions and by default append `Options`, e.g., `getPetByIdOptions()`. +Queries are generated from [query operations](/docs/openapi/typescript/configuration/parser#hooks-query-operations). The generated query functions follow the naming convention of SDK functions and by default append `Options`, e.g., `getPetByIdOptions()`. @@ -328,7 +328,7 @@ You can customize the naming and casing pattern for `queryKeys` functions using ## Infinite Queries -Infinite queries are generated from [query operations](/openapi-ts/configuration/parser#hooks-query-operations) if we detect a [pagination](/openapi-ts/configuration/parser#pagination) parameter. The generated infinite query functions follow the naming convention of SDK functions and by default append `InfiniteOptions`, e.g., `getFooInfiniteOptions()`. +Infinite queries are generated from [query operations](/docs/openapi/typescript/configuration/parser#hooks-query-operations) if we detect a [pagination](/docs/openapi/typescript/configuration/parser#pagination) parameter. The generated infinite query functions follow the naming convention of SDK functions and by default append `InfiniteOptions`, e.g., `getFooInfiniteOptions()`. @@ -536,7 +536,7 @@ You can customize the naming and casing pattern for `infiniteQueryKeys` function ## Mutations -Mutations are generated from [mutation operations](/openapi-ts/configuration/parser#hooks-mutation-operations). The generated mutation functions follow the naming convention of SDK functions and by default append `Mutation`, e.g., `addPetMutation()`. +Mutations are generated from [mutation operations](/docs/openapi/typescript/configuration/parser#hooks-mutation-operations). The generated mutation functions follow the naming convention of SDK functions and by default append `Mutation`, e.g., `addPetMutation()`. diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/transformers.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/transformers.mdx index 016f949629..d2bbe954cd 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/transformers.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/transformers.mdx @@ -5,12 +5,14 @@ description: Learn about transforming data with @hey-api/openapi-ts. import { Tabs, TabItem } from '@astrojs/starlight/components'; -::: tip Using Zod instead of `@hey-api/transformers` +:::tip +title="Using Zod instead of `@hey-api/transformers`" + This page covers the `@hey-api/transformers` plugin, which provides a built-in way to transform response data. If you're already using the Zod plugin for validation, you may also use Zod as your response transformer. This lets Zod's `parseAsync` handle response transformation directly. -See the [Zod plugin docs](/openapi-ts/plugins/zod#zod-as-a-response-transformer) for details. +See the [Zod plugin docs](/docs/openapi/typescript/plugins/zod#zod-as-a-response-transformer) for details. ::: # Transformers @@ -39,7 +41,7 @@ If your data isn't being transformed as expected, we encourage you to leave feed ## Installation -In your [configuration](/openapi-ts/get-started), add `@hey-api/transformers` to your plugins and you'll be ready to generate transformers. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/transformers` to your plugins and you'll be ready to generate transformers. 🎉 ```js {6} title="openapi-ts.config.ts" export default { diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/typescript.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/typescript.mdx index 2414ed6e11..ee92c2b464 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/typescript.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/typescript.mdx @@ -14,7 +14,7 @@ TypeScript interfaces are located in the `types.gen.ts` file. This is the only f ## Installation -In your [configuration](/openapi-ts/get-started), add `@hey-api/typescript` to your plugins and you'll be ready to generate TypeScript artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `@hey-api/typescript` to your plugins and you'll be ready to generate TypeScript artifacts. 🎉 ```js {6} title="openapi-ts.config.ts" export default { @@ -176,7 +176,7 @@ export default { ## Resolvers -You can further customize this plugin's behavior using [resolvers](/openapi-ts/plugins/concepts/resolvers). +You can further customize this plugin's behavior using [resolvers](/docs/openapi/typescript/plugins/concepts/resolvers). ## API diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/valibot.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/valibot.mdx index 6c20b058e9..e652a459dc 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/valibot.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/valibot.mdx @@ -27,7 +27,7 @@ The Valibot plugin for Hey API generates schemas from your OpenAPI spec, fully c ## Installation -In your [configuration](/openapi-ts/get-started), add `valibot` to your plugins and you'll be ready to generate Valibot artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `valibot` to your plugins and you'll be ready to generate Valibot artifacts. 🎉 ```js {6} title="openapi-ts.config.ts" export default { @@ -42,9 +42,11 @@ export default { ### SDKs -To add data validators to your SDKs, set `sdk.validator` to `true`. +To add data validators or transformers to your SDKs, set `validator` or `transformer` to `true`. -```js {8-9} title="openapi-ts.config.ts" + + +```js {7-10} title="openapi-ts.config.ts" export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: 'src/client', @@ -58,8 +60,26 @@ export default { ], }; ``` + + +```js {7-10} title="openapi-ts.config.ts" +export default { + input: 'hey-api/backend', // sign up at app.heyapi.dev + output: 'src/client', + plugins: [ + // ...other plugins + 'valibot', + { + name: '@hey-api/sdk', + transformer: true, + }, + ], +}; +``` + + -Learn more about data validators in your SDKs on the [SDKs](/openapi-ts/plugins/sdk#validators) page. +The SDK page contains more information about data [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers). ## Output @@ -241,7 +261,7 @@ export default { ## Resolvers -You can further customize this plugin's behavior using [resolvers](/openapi-ts/plugins/concepts/resolvers). +You can further customize this plugin's behavior using [resolvers](/docs/openapi/typescript/plugins/concepts/resolvers). ## API diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/zod/mini.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/zod/mini.mdx index db7bc24835..c307203d1d 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/zod/mini.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/zod/mini.mdx @@ -25,9 +25,9 @@ The Zod plugin for Hey API generates schemas from your OpenAPI spec, fully compa ## Installation -In your [configuration](/openapi-ts/get-started), add `zod` to your plugins and you'll be ready to generate Zod artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `zod` to your plugins and you'll be ready to generate Zod artifacts. 🎉 -```js {7-8} title="openapi-ts.config.ts" +```js {6-9} title="openapi-ts.config.ts" export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: 'src/client', @@ -43,9 +43,11 @@ export default { ### SDKs -To add data validators to your SDKs, set `sdk.validator` to `true`. +To add data validators or transformers to your SDKs, set `validator` or `transformer` to `true`. -```js {11-12} title="openapi-ts.config.ts" + + +```js {10-13} title="openapi-ts.config.ts" export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: 'src/client', @@ -62,8 +64,29 @@ export default { ], }; ``` + + +```js {10-13} title="openapi-ts.config.ts" +export default { + input: 'hey-api/backend', // sign up at app.heyapi.dev + output: 'src/client', + plugins: [ + // ...other plugins + { + name: 'zod', + compatibilityVersion: 'mini', + }, + { + name: '@hey-api/sdk', + transformer: true, + }, + ], +}; +``` + + -Learn more about data validators in your SDKs on the [SDKs](/openapi-ts/plugins/sdk#validators) page. +The SDK page contains more information about data [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers). ## Output @@ -345,7 +368,7 @@ You can customize the naming and casing pattern for schema-specific `types` usin ## Resolvers -You can further customize this plugin's behavior using [resolvers](/openapi-ts/plugins/concepts/resolvers). +You can further customize this plugin's behavior using [resolvers](/docs/openapi/typescript/plugins/concepts/resolvers). ## API diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/zod/v3.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/zod/v3.mdx index 0dba072b18..8d020ea5f9 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/zod/v3.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/zod/v3.mdx @@ -25,9 +25,9 @@ The Zod plugin for Hey API generates schemas from your OpenAPI spec, fully compa ## Installation -In your [configuration](/openapi-ts/get-started), add `zod` to your plugins and you'll be ready to generate Zod artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `zod` to your plugins and you'll be ready to generate Zod artifacts. 🎉 -```js {7-8} title="openapi-ts.config.ts" +```js {6-9} title="openapi-ts.config.ts" export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: 'src/client', @@ -43,9 +43,11 @@ export default { ### SDKs -To add data validators to your SDKs, set `sdk.validator` to `true`. +To add data validators or transformers to your SDKs, set `validator` or `transformer` to `true`. -```js {11-12} title="openapi-ts.config.ts" + + +```js {10-13} title="openapi-ts.config.ts" export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: 'src/client', @@ -62,8 +64,29 @@ export default { ], }; ``` + + +```js {10-13} title="openapi-ts.config.ts" +export default { + input: 'hey-api/backend', // sign up at app.heyapi.dev + output: 'src/client', + plugins: [ + // ...other plugins + { + name: 'zod', + compatibilityVersion: 3, + }, + { + name: '@hey-api/sdk', + transformer: true, + }, + ], +}; +``` + + -Learn more about data validators in your SDKs on the [SDKs](/openapi-ts/plugins/sdk#validators) page. +The SDK page contains more information about data [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers). ## Output @@ -311,7 +334,7 @@ You can customize the naming and casing pattern for schema-specific `types` usin ## Resolvers -You can further customize this plugin's behavior using [resolvers](/openapi-ts/plugins/concepts/resolvers). +You can further customize this plugin's behavior using [resolvers](/docs/openapi/typescript/plugins/concepts/resolvers). ## API diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx index 1020a4b88f..b6f42a6f31 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx @@ -27,7 +27,7 @@ The Zod plugin for Hey API generates schemas from your OpenAPI spec, fully compa ## Installation -In your [configuration](/openapi-ts/get-started), add `zod` to your plugins and you'll be ready to generate Zod artifacts. 🎉 +In your [configuration](/docs/openapi/typescript/get-started), add `zod` to your plugins and you'll be ready to generate Zod artifacts. 🎉 ```js {6} title="openapi-ts.config.ts" export default { @@ -42,9 +42,11 @@ export default { ### SDKs -To add data validators to your SDKs, set `sdk.validator` to `true`. +To add data validators or transformers to your SDKs, set `validator` or `transformer` to `true`. -```js {8-9} title="openapi-ts.config.ts" + + +```js {7-10} title="openapi-ts.config.ts" export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: 'src/client', @@ -58,14 +60,9 @@ export default { ], }; ``` - -Learn more about data validators in your SDKs on the [SDKs](/openapi-ts/plugins/sdk#validators) page. - -### Zod as a response transformer - -You can also use Zod as a **response transformer** by setting `sdk.transformer` to `'zod'`. This causes the SDK to call `parseAsync` on the response and use its return value, so Zod's coercion and transformation rules are applied before the data is returned to you. - -```js + + +```js {7-10} title="openapi-ts.config.ts" export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: 'src/client', @@ -73,24 +70,16 @@ export default { // ...other plugins 'zod', { - name: '@hey-api/sdk', // [!code ++] - transformer: 'zod', // [!code ++] + name: '@hey-api/sdk', + transformer: true, }, ], }; ``` + + -This generates an inline `responseTransformer` in each SDK function: - -```ts -// sdk.gen.ts (generated) -export const getFoo = (options?) => - client.get({ - responseTransformer: async (data) => await zGetFooResponse.parseAsync(data), - url: '/foo', - ...options, - }); -``` +The SDK page contains more information about data [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers). ## Output @@ -364,7 +353,7 @@ You can customize the naming and casing pattern for schema-specific `types` usin ## Resolvers -You can further customize this plugin's behavior using [resolvers](/openapi-ts/plugins/concepts/resolvers). +You can further customize this plugin's behavior using [resolvers](/docs/openapi/typescript/plugins/concepts/resolvers). ## API diff --git a/web/src/content/docs/docs/openapi/typescript/state-management.mdx b/web/src/content/docs/docs/openapi/typescript/state-management.mdx index 1bc6c0c938..4e54a8d80f 100644 --- a/web/src/content/docs/docs/openapi/typescript/state-management.mdx +++ b/web/src/content/docs/docs/openapi/typescript/state-management.mdx @@ -16,10 +16,10 @@ Any reasonably large application will have to deal with state management at some Hey API natively supports the following state managers. -- [Pinia Colada](/openapi-ts/plugins/pinia-colada) -- [TanStack Query](/openapi-ts/plugins/tanstack-query) -- [SWR](/openapi-ts/plugins/swr) -- [Zustand](/openapi-ts/plugins/zustand) +- [Pinia Colada](/docs/openapi/typescript/plugins/pinia-colada) +- [TanStack Query](/docs/openapi/typescript/plugins/tanstack-query) +- [SWR](/docs/openapi/typescript/plugins/swr) +- [Zustand](/docs/openapi/typescript/plugins/zustand) Don't see your state manager? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). diff --git a/web/src/content/docs/docs/openapi/typescript/validators.mdx b/web/src/content/docs/docs/openapi/typescript/validators.mdx index e0f8a9325d..b70adf435f 100644 --- a/web/src/content/docs/docs/openapi/typescript/validators.mdx +++ b/web/src/content/docs/docs/openapi/typescript/validators.mdx @@ -23,13 +23,13 @@ Whatever your reason to use validators might be, you can rest assured that you'r Hey API natively supports the following validators. -- [Valibot](/openapi-ts/plugins/valibot) -- [Zod](/openapi-ts/plugins/zod) -- [Ajv](/openapi-ts/plugins/ajv) -- [Arktype](/openapi-ts/plugins/arktype) -- [Joi](/openapi-ts/plugins/joi) -- [TypeBox](/openapi-ts/plugins/typebox) -- [Yup](/openapi-ts/plugins/yup) +- [Valibot](/docs/openapi/typescript/plugins/valibot) +- [Zod](/docs/openapi/typescript/plugins/zod) +- [Ajv](/docs/openapi/typescript/plugins/ajv) +- [Arktype](/docs/openapi/typescript/plugins/arktype) +- [Joi](/docs/openapi/typescript/plugins/joi) +- [TypeBox](/docs/openapi/typescript/plugins/typebox) +- [Yup](/docs/openapi/typescript/plugins/yup) Don't see your validator? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). diff --git a/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx b/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx index a684bfab2b..73fc4b3cf9 100644 --- a/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx +++ b/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx @@ -16,15 +16,15 @@ There are two approaches to developing APIs: code-first, where you start with th Hey API natively supports the following frameworks. -- [Angular](/openapi-ts/plugins/angular) -- [Fastify](/openapi-ts/plugins/fastify) -- [Nest](/openapi-ts/plugins/nest) -- [oRPC](/openapi-ts/plugins/orpc) -- [Adonis](/openapi-ts/plugins/adonis) -- [Elysia](/openapi-ts/plugins/elysia) -- [Express](/openapi-ts/plugins/express) -- [Hono](/openapi-ts/plugins/hono) -- [Koa](/openapi-ts/plugins/koa) +- [Angular](/docs/openapi/typescript/plugins/angular) +- [Fastify](/docs/openapi/typescript/plugins/fastify) +- [Nest](/docs/openapi/typescript/plugins/nest) +- [oRPC](/docs/openapi/typescript/plugins/orpc) +- [Adonis](/docs/openapi/typescript/plugins/adonis) +- [Elysia](/docs/openapi/typescript/plugins/elysia) +- [Express](/docs/openapi/typescript/plugins/express) +- [Hono](/docs/openapi/typescript/plugins/hono) +- [Koa](/docs/openapi/typescript/plugins/koa) Don't see your framework? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). From 31c7532f0ec131a220f3c4662d8bbcdba1d360ac Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 16 May 2026 21:33:05 +0800 Subject: [PATCH 13/14] docs: add transformers section --- .../docs/docs/openapi/typescript/core.mdx | 2 +- .../docs/docs/openapi/typescript/mocks.mdx | 2 +- .../docs/openapi/typescript/plugins/orpc.mdx | 2 +- .../docs/openapi/typescript/plugins/sdk.mdx | 40 ++++++++++++++++++- .../typescript/plugins/transformers.mdx | 20 +++------- .../openapi/typescript/plugins/typescript.mdx | 2 +- .../openapi/typescript/plugins/valibot.mdx | 4 +- .../openapi/typescript/plugins/zod/mini.mdx | 4 +- .../openapi/typescript/plugins/zod/v3.mdx | 4 +- .../openapi/typescript/plugins/zod/v4.mdx | 4 +- .../openapi/typescript/state-management.mdx | 2 +- .../docs/openapi/typescript/validators.mdx | 10 +++-- .../openapi/typescript/web-frameworks.mdx | 2 +- 13 files changed, 64 insertions(+), 34 deletions(-) diff --git a/web/src/content/docs/docs/openapi/typescript/core.mdx b/web/src/content/docs/docs/openapi/typescript/core.mdx index c4abfddd3c..839f7d1894 100644 --- a/web/src/content/docs/docs/openapi/typescript/core.mdx +++ b/web/src/content/docs/docs/openapi/typescript/core.mdx @@ -20,6 +20,6 @@ Hey API provides the following core plugins. - [Transformers](/docs/openapi/typescript/plugins/transformers) - [Schemas](/docs/openapi/typescript/plugins/schemas) -Need another core plugin? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). +Need another core plugin? Let us know by [opening an issue](https://github.com/hey-api/openapi-ts/issues). diff --git a/web/src/content/docs/docs/openapi/typescript/mocks.mdx b/web/src/content/docs/docs/openapi/typescript/mocks.mdx index e01ca99f82..bfd75dc517 100644 --- a/web/src/content/docs/docs/openapi/typescript/mocks.mdx +++ b/web/src/content/docs/docs/openapi/typescript/mocks.mdx @@ -23,6 +23,6 @@ Hey API natively supports the following mocking frameworks. - [Nock](/docs/openapi/typescript/plugins/nock) - [Supertest](/docs/openapi/typescript/plugins/supertest) -Don't see your framework? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). +Don't see your framework? Let us know by [opening an issue](https://github.com/hey-api/openapi-ts/issues). diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/orpc.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/orpc.mdx index 317bc539f6..b0920243a8 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/orpc.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/orpc.mdx @@ -89,7 +89,7 @@ export default { ### Validators -To enable schema validation, set `validator` to `zod` or one of the available [validator plugins](/docs/openapi/typescript/validators). This will implicitly add the selected plugin with default values. +To enable schema validation, set `validator` to `zod` or one of the available [validator plugins](/docs/openapi/typescript/validators). This will automatically add the selected plugin with its default configuration. For a more granular approach, manually add a validator plugin and set `validator` to the plugin name or `true` to automatically select a compatible plugin. Until you customize the validator plugin, both approaches will produce the same default output. diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/sdk.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/sdk.mdx index 6a1f7fb0d1..14183e8a5b 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/sdk.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/sdk.mdx @@ -241,7 +241,7 @@ The SDK plugin currently supports only the `bearer` and `basic` auth schemes. [O ## Validators -Validating data at runtime comes with a performance cost, which is why it's not enabled by default. To enable validation, set `validator` to `zod` or one of the available [validator plugins](/docs/openapi/typescript/validators). This will implicitly add the selected plugin with default values. +Validating data at runtime comes with a performance cost, which is why it's not enabled by default. To enable validation, set `validator` to `zod` or one of the available [validator plugins](/docs/openapi/typescript/validators). This will automatically add the selected plugin with its default configuration. For a more granular approach, manually add a validator plugin and set `validator` to the plugin name or `true` to automatically select a compatible plugin. Until you customize the validator plugin, both approaches will produce the same default output. @@ -324,7 +324,43 @@ Learn more about available validators on the [Validators](/docs/openapi/typescri ## Transformers -TODO +Transformers work similarly to [validators](#validators), with two differences: transformation applies to responses only, and you use the `transformer` option instead of `validator`. + + + +```ts {5-6} title="sdk.gen.ts" +import * as v from 'valibot'; + +export const addPet = (options: Options) => + (options.client ?? client).post({ + responseTransformer: async (data) => + await v.parseAsync(vAddPetResponse, data), + /** ... */ + }); +``` + + +```js {8,11} title="openapi-ts.config.ts" +export default { + input: 'hey-api/backend', // sign up at app.heyapi.dev + output: 'src/client', + plugins: [ + // ...other plugins + { + name: '@hey-api/sdk', + transformer: true, // or 'valibot' + }, + { + name: 'valibot', // customize (optional) + // other options + }, + ], +}; +``` + + + +Learn more about available transformers on the [Validators](/docs/openapi/typescript/validators) page. ## Code Examples diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/transformers.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/transformers.mdx index d2bbe954cd..08898cc501 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/transformers.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/transformers.mdx @@ -5,26 +5,18 @@ description: Learn about transforming data with @hey-api/openapi-ts. import { Tabs, TabItem } from '@astrojs/starlight/components'; -:::tip -title="Using Zod instead of `@hey-api/transformers`" - -This page covers the `@hey-api/transformers` plugin, which provides a built-in way to transform response data. +# Transformers -If you're already using the Zod plugin for validation, you may also use Zod as your response transformer. This lets Zod's `parseAsync` handle response transformation directly. +:::tip +If you're already using a [validator](/docs/openapi/typescript/validators) plugin such as Zod or Valibot, you can use it as a transformer too. See your validator plugin's docs for details. -See the [Zod plugin docs](/docs/openapi/typescript/plugins/zod#zod-as-a-response-transformer) for details. +Otherwise, `@hey-api/transformers` is the right choice. ::: -# Transformers - JSON is the most commonly used data format in REST APIs. However, it does not map well to complex data types. For example, both regular strings and date strings become simple strings in JSON. One approach to this problem is using a [JSON superset](https://github.com/blitz-js/superjson). For most people, switching formats is not feasible. That's why we provide the `@hey-api/transformers` plugin. -:::caution -Transformers currently handle only the most common use cases. If your data isn't being transformed as expected, we encourage you to leave feedback on [GitHub](https://github.com/hey-api/openapi-ts/issues). -::: - ## Considerations Before deciding whether transformers are right for you, let's explain how they work. Transformers generate a runtime file, therefore they impact the bundle size. We generate a single transformer per operation response for the most efficient result, just like a human engineer would. @@ -56,9 +48,9 @@ export default { ## SDKs -To automatically transform response data in your SDKs, set `sdk.transformer` to `true`. +To transform response data in your SDKs, set `transformer` to `true` on the SDK plugin. -```js {8-9} title="openapi-ts.config.ts" +```js {7-10} title="openapi-ts.config.ts" export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: 'src/client', diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/typescript.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/typescript.mdx index ee92c2b464..4f140a262f 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/typescript.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/typescript.mdx @@ -28,7 +28,7 @@ export default { ``` :::note -The `@hey-api/typescript` plugin might be implicitly added to your `plugins` if another plugin depends on it. +The `@hey-api/typescript` plugin might be automatically added to your `plugins` if another plugin depends on it. ::: ## Output diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/valibot.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/valibot.mdx index e652a459dc..b842f53fb0 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/valibot.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/valibot.mdx @@ -42,7 +42,7 @@ export default { ### SDKs -To add data validators or transformers to your SDKs, set `validator` or `transformer` to `true`. +Valibot can validate or transform data directly in your SDK. Set `validator` or `transformer` to `true` on the SDK plugin to enable it. @@ -79,7 +79,7 @@ export default { -The SDK page contains more information about data [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers). +The SDK page covers [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers) in more detail. ## Output diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/zod/mini.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/zod/mini.mdx index c307203d1d..9047c59632 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/zod/mini.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/zod/mini.mdx @@ -43,7 +43,7 @@ export default { ### SDKs -To add data validators or transformers to your SDKs, set `validator` or `transformer` to `true`. +Zod can validate or transform data directly in your SDK. Set `validator` or `transformer` to `true` on the SDK plugin to enable it. @@ -86,7 +86,7 @@ export default { -The SDK page contains more information about data [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers). +The SDK page covers [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers) in more detail. ## Output diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/zod/v3.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/zod/v3.mdx index 8d020ea5f9..c37a08d660 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/zod/v3.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/zod/v3.mdx @@ -43,7 +43,7 @@ export default { ### SDKs -To add data validators or transformers to your SDKs, set `validator` or `transformer` to `true`. +Zod can validate or transform data directly in your SDK. Set `validator` or `transformer` to `true` on the SDK plugin to enable it. @@ -86,7 +86,7 @@ export default { -The SDK page contains more information about data [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers). +The SDK page covers [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers) in more detail. ## Output diff --git a/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx b/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx index b6f42a6f31..4fbb186c4e 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/zod/v4.mdx @@ -42,7 +42,7 @@ export default { ### SDKs -To add data validators or transformers to your SDKs, set `validator` or `transformer` to `true`. +Zod can validate or transform data directly in your SDK. Set `validator` or `transformer` to `true` on the SDK plugin to enable it. @@ -79,7 +79,7 @@ export default { -The SDK page contains more information about data [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers). +The SDK page covers [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers) in more detail. ## Output diff --git a/web/src/content/docs/docs/openapi/typescript/state-management.mdx b/web/src/content/docs/docs/openapi/typescript/state-management.mdx index 4e54a8d80f..aa9a2a243d 100644 --- a/web/src/content/docs/docs/openapi/typescript/state-management.mdx +++ b/web/src/content/docs/docs/openapi/typescript/state-management.mdx @@ -21,6 +21,6 @@ Hey API natively supports the following state managers. - [SWR](/docs/openapi/typescript/plugins/swr) - [Zustand](/docs/openapi/typescript/plugins/zustand) -Don't see your state manager? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). +Don't see your state manager? Let us know by [opening an issue](https://github.com/hey-api/openapi-ts/issues). diff --git a/web/src/content/docs/docs/openapi/typescript/validators.mdx b/web/src/content/docs/docs/openapi/typescript/validators.mdx index b70adf435f..8597086f98 100644 --- a/web/src/content/docs/docs/openapi/typescript/validators.mdx +++ b/web/src/content/docs/docs/openapi/typescript/validators.mdx @@ -1,6 +1,6 @@ --- title: Validators -description: Learn about validating data with @hey-api/openapi-ts. +description: Learn about validating and transforming data with @hey-api/openapi-ts. sidebar: label: Overview --- @@ -12,14 +12,16 @@ import Examples from '@/components/Examples.astro'; There are times when you cannot blindly trust the server to return the correct data. You might be working on a critical application where any mistakes would be costly, or you're simply dealing with a legacy or undocumented system. -Whatever your reason to use validators might be, you can rest assured that you're working with the correct data. +Validator plugins generate schemas from your input. You can connect them to your client as [validators](/docs/openapi/typescript/plugins/sdk#validators) or [transformers](/docs/openapi/typescript/plugins/sdk#transformers) via the SDK plugin. ## Features - seamless integration with `@hey-api/openapi-ts` ecosystem - schemas for requests, responses, and reusable definitions +- static type inference from schemas +- tree-shakeable output -## Options +## Plugins Hey API natively supports the following validators. @@ -31,6 +33,6 @@ Hey API natively supports the following validators. - [TypeBox](/docs/openapi/typescript/plugins/typebox) - [Yup](/docs/openapi/typescript/plugins/yup) -Don't see your validator? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). +Don't see your validator? Let us know by [opening an issue](https://github.com/hey-api/openapi-ts/issues). diff --git a/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx b/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx index 73fc4b3cf9..bd8a69afdc 100644 --- a/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx +++ b/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx @@ -26,6 +26,6 @@ Hey API natively supports the following frameworks. - [Hono](/docs/openapi/typescript/plugins/hono) - [Koa](/docs/openapi/typescript/plugins/koa) -Don't see your framework? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). +Don't see your framework? Let us know by [opening an issue](https://github.com/hey-api/openapi-ts/issues). From 63eb76c4b4d8ee7db781b5537e09515249b0de57 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 16 May 2026 21:35:15 +0800 Subject: [PATCH 14/14] docs: rename options heading to plugins --- web/src/content/docs/docs/openapi/typescript/clients.mdx | 2 +- web/src/content/docs/docs/openapi/typescript/core.mdx | 2 +- web/src/content/docs/docs/openapi/typescript/mocks.mdx | 2 +- .../content/docs/docs/openapi/typescript/state-management.mdx | 2 +- web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/src/content/docs/docs/openapi/typescript/clients.mdx b/web/src/content/docs/docs/openapi/typescript/clients.mdx index 78a0eca903..8a5598da63 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients.mdx @@ -22,7 +22,7 @@ We all send HTTP requests in a slightly different way. Hey API doesn't force you - minimal learning curve thanks to extending the underlying technology - support bundling inside the generated output -## Options +## Plugins Hey API natively supports the following clients. diff --git a/web/src/content/docs/docs/openapi/typescript/core.mdx b/web/src/content/docs/docs/openapi/typescript/core.mdx index 839f7d1894..f2a88affbd 100644 --- a/web/src/content/docs/docs/openapi/typescript/core.mdx +++ b/web/src/content/docs/docs/openapi/typescript/core.mdx @@ -11,7 +11,7 @@ import Examples from '@/components/Examples.astro'; Apart from being responsible for the default output, core plugins are the foundation for other plugins. Instead of creating their own primitives, other plugins can reuse the artifacts from core plugins. This results in a smaller output size and a better user experience. -## Options +## Plugins Hey API provides the following core plugins. diff --git a/web/src/content/docs/docs/openapi/typescript/mocks.mdx b/web/src/content/docs/docs/openapi/typescript/mocks.mdx index bfd75dc517..3de9cc14c4 100644 --- a/web/src/content/docs/docs/openapi/typescript/mocks.mdx +++ b/web/src/content/docs/docs/openapi/typescript/mocks.mdx @@ -12,7 +12,7 @@ import Examples from '@/components/Examples.astro'; Realistic mock data is an important component of every robust development process, testing strategy, and product presentation. -## Options +## Plugins Hey API natively supports the following mocking frameworks. diff --git a/web/src/content/docs/docs/openapi/typescript/state-management.mdx b/web/src/content/docs/docs/openapi/typescript/state-management.mdx index aa9a2a243d..a598d32381 100644 --- a/web/src/content/docs/docs/openapi/typescript/state-management.mdx +++ b/web/src/content/docs/docs/openapi/typescript/state-management.mdx @@ -12,7 +12,7 @@ import Examples from '@/components/Examples.astro'; Any reasonably large application will have to deal with state management at some point. State-related code is often one of the biggest boilerplates in your codebase. Well, at least until you start using our state management plugins. -## Options +## Plugins Hey API natively supports the following state managers. diff --git a/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx b/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx index bd8a69afdc..5636a813b9 100644 --- a/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx +++ b/web/src/content/docs/docs/openapi/typescript/web-frameworks.mdx @@ -12,7 +12,7 @@ import Examples from '@/components/Examples.astro'; There are two approaches to developing APIs: code-first, where you start with the code, or spec-first, where you begin with the specification. If you use the latter, you can ensure your APIs adhere to the specification with our web framework plugins. -## Options +## Plugins Hey API natively supports the following frameworks.