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/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/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 13e5671aef..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,8 +163,6 @@ describe(`OpenAPI ${version}`, () => { input: 'type-format.yaml', 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/__snapshots__/3.1.x/mini/transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/client.gen.ts new file mode 100644 index 0000000000..fc3f037f16 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/client.gen.ts @@ -0,0 +1,277 @@ +// 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/transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/utils.gen.ts new file mode 100644 index 0000000000..7800fe4b9d --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/client/utils.gen.ts @@ -0,0 +1,316 @@ +// 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/transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/sdk.gen.ts new file mode 100644 index 0000000000..0c0e6947a0 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/sdk.gen.ts @@ -0,0 +1,33 @@ +// 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), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/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/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/transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/client.gen.ts new file mode 100644 index 0000000000..fc3f037f16 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/client.gen.ts @@ -0,0 +1,277 @@ +// 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/transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/utils.gen.ts new file mode 100644 index 0000000000..7800fe4b9d --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/client/utils.gen.ts @@ -0,0 +1,316 @@ +// 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/transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/sdk.gen.ts new file mode 100644 index 0000000000..c3cf00d20b --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/sdk.gen.ts @@ -0,0 +1,33 @@ +// 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), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/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/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/transformer/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/client/client.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/client.gen.ts new file mode 100644 index 0000000000..fc3f037f16 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/client.gen.ts @@ -0,0 +1,277 @@ +// 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/transformer/client/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/client/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/client/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/utils.gen.ts new file mode 100644 index 0000000000..7800fe4b9d --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/client/utils.gen.ts @@ -0,0 +1,316 @@ +// 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/transformer/core/auth.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/core/bodySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/core/params.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/core/pathSerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/core/serverSentEvents.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/core/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/core/utils.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/index.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/index.ts new file mode 100644 index 0000000000..8dc6d04993 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/transformer/sdk.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/sdk.gen.ts new file mode 100644 index 0000000000..ab9d05b5ba --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/sdk.gen.ts @@ -0,0 +1,33 @@ +// 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), + url: '/foo', + ...options +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/transformer/types.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/transformer/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/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/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/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts index 1ee06b242b..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 @@ -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: true, + validator: true, + }, + ], + }), + description: 'handles various schema types and formats', + }, ]; it.each(scenarios)('$description', async ({ config }) => { 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..5713405c74 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts @@ -42,20 +42,26 @@ export const defaultConfig: HeyApiSdkPlugin['Config'] = { plugin.config.client = false; } - if (plugin.config.transformer) { - if (typeof plugin.config.transformer === 'boolean') { + if (typeof plugin.config.transformer !== 'object') { + plugin.config.transformer = { + response: plugin.config.transformer, + }; + } + + if (plugin.config.transformer.response) { + if (typeof plugin.config.transformer.response === 'boolean') { try { - plugin.config.transformer = context.pluginByTag('transformer'); - plugin.dependencies.add(plugin.config.transformer!); + plugin.config.transformer.response = context.pluginByTag('transformer'); + plugin.dependencies.add(plugin.config.transformer.response!); } catch { log.warn(transformerInferWarn); - plugin.config.transformer = false; + plugin.config.transformer.response = false; } } 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/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 3e54a36257..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 @@ -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'; @@ -11,8 +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 { createRequestValidator, createResponseValidator } from './validator'; +import { createRequestValidator } from './validator'; /** TODO: needs complete refactor */ export const operationOptionsType = ({ @@ -334,22 +334,14 @@ export function operationStatements({ } const requestValidator = createRequestValidator({ operation, plugin }); - const responseValidator = createResponseValidator({ operation, plugin }); + const responseHandlers = createResponseHandlers({ operation, plugin }); + if (requestValidator) { - reqOptions.prop('requestValidator', requestValidator.arrow()); + reqOptions.prop('requestValidator', requestValidator); } - 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 (responseHandlers.transformer) { + reqOptions.prop('responseTransformer', responseHandlers.transformer); } let hasServerSentEvents = false; @@ -376,8 +368,8 @@ export function operationStatements({ } } - if (responseValidator) { - reqOptions.prop('responseValidator', responseValidator.arrow()); + 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 new file mode 100644 index 0000000000..f4f1af7dd2 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/shared/transformer.ts @@ -0,0 +1,33 @@ +import type { SymbolMeta } from '@hey-api/codegen-core'; + +import { $ } from '../../../../ts-dsl'; +import type { ResponseHandlers, ValidatorArgs } from './types'; + +export function createResponseTransformer({ + operation, + plugin, +}: ValidatorArgs): ResponseHandlers['transformer'] { + if (!plugin.config.transformer.response) return; + + const transformer = plugin.getPluginOrThrow(plugin.config.transformer.response); + if ( + transformer.api?.createResponseTransformer && + typeof transformer.api.createResponseTransformer === 'function' + ) { + return transformer.api.createResponseTransformer({ + operation, + plugin: transformer, + }); + } + + 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 c66fac3303..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,12 +1,15 @@ -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' }>; 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); @@ -22,12 +25,7 @@ export function createRequestValidator({ export function createResponseValidator({ operation, plugin, -}: { - /** The operation object. */ - operation: IR.OperationObject; - /** The plugin instance. */ - plugin: HeyApiSdkPlugin['Instance']; -}): ReturnType | 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/@hey-api/sdk/types.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.ts index ec5497d6c7..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,13 +76,29 @@ 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. + * 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. + * + * Ensure you have declared the selected library as a dependency to avoid + * errors. * * @default false */ - transformer?: PluginTransformerNames | boolean; + transformer?: + | PluginTransformerNames + | boolean + | { + /** + * Transform response data before returning. + * + * Can be a transformer plugin name or boolean (true to auto-select, false + * to disable). + * + * @default false + */ + response?: PluginTransformerNames | 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 @@ -195,40 +211,15 @@ 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** @@ -238,36 +229,16 @@ export type Config = Plugin.Name<'@hey-api/sdk'> & * @default 'fields' */ 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 - */ - transformer: 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. - */ + /** Transform response data before returning. */ + transformer: { + /** 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. */ 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/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/types.ts b/packages/openapi-ts/src/plugins/types.ts index 4da1edded1..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'; +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 47d0b227f2..0ae51dceff 100644 --- a/packages/openapi-ts/src/plugins/valibot/api.ts +++ b/packages/openapi-ts/src/plugins/valibot/api.ts @@ -8,37 +8,54 @@ import type { ValibotPlugin } from './types'; import { createRequestSchemaV1, createRequestValidatorV1, + createResponseHandlersV1, + createResponseTransformerV1, 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; + ctx: RequestSchemaContext, + ) => 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, - ): ReturnType | 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): ReturnType | 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 8fadfaf35d..1d89a0ada4 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', @@ -232,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 097ec11769..bc1ba1824a 100644 --- a/packages/openapi-ts/src/plugins/zod/api.ts +++ b/packages/openapi-ts/src/plugins/zod/api.ts @@ -5,6 +5,8 @@ import type { $ } from '../../ts-dsl'; import { createRequestSchemaMini, createRequestValidatorMini, + createResponseHandlersMini, + createResponseTransformerMini, createResponseValidatorMini, } from './mini/api'; import type { Chain } from './shared/chain'; @@ -13,28 +15,39 @@ import type { ZodPlugin } from './types'; import { createRequestSchemaV3, createRequestValidatorV3, + createResponseHandlersV3, + createResponseTransformerV3, createResponseValidatorV3, } from './v3/api'; import { createRequestSchemaV4, createRequestValidatorV4, + createResponseHandlersV4, + createResponseTransformerV4, createResponseValidatorV4, } from './v4/api'; +type ArrowFunc = Extract, { '~mode': 'arrow' }>; + export type IApi = { createRequestSchema: ( ctx: RequestSchemaContext, ) => Symbol | Chain | undefined; createRequestValidator: ( ctx: RequestSchemaContext, - ) => ReturnType | undefined; - createResponseValidator: (ctx: ValidatorArgs) => ReturnType | undefined; + ) => 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 | Chain | undefined { + ): ReturnType { const { plugin } = ctx; if (!plugin.config.requests.enabled) return; switch (plugin.config.compatibilityVersion) { @@ -50,7 +63,7 @@ export class Api implements IApi { createRequestValidator( ctx: RequestSchemaContext, - ): ReturnType | undefined { + ): ReturnType { const { plugin } = ctx; if (!plugin.config.requests.enabled) return; switch (plugin.config.compatibilityVersion) { @@ -64,7 +77,33 @@ export class Api implements IApi { } } - createResponseValidator(ctx: ValidatorArgs): ReturnType | 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: + return createResponseTransformerV3(ctx); + case 'mini': + return createResponseTransformerMini(ctx); + case 4: + default: + return createResponseTransformerV4(ctx); + } + } + + createResponseValidator(ctx: ValidatorArgs): ReturnType { const { plugin } = ctx; switch (plugin.config.compatibilityVersion) { case 3: 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'], }; /** diff --git a/packages/openapi-ts/src/plugins/zod/mini/api.ts b/packages/openapi-ts/src/plugins/zod/mini/api.ts index 1a6a8635f1..49dd7165a0 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', @@ -230,3 +228,17 @@ export function createResponseValidatorMini({ }; return runResponseResolver(resolverCtx); } + +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 41ef4a4398..1fbc92d7f5 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', @@ -229,3 +227,17 @@ export function createResponseValidatorV3({ }; return runResponseResolver(resolverCtx); } + +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 ef794218d1..f3fcf803f9 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', @@ -229,3 +227,17 @@ export function createResponseValidatorV4({ }; return runResponseResolver(resolverCtx); } + +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/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; diff --git a/web/src/content/docs/docs/openapi/typescript/clients.mdx b/web/src/content/docs/docs/openapi/typescript/clients.mdx index 0d1b281b5b..8a5598da63 100644 --- a/web/src/content/docs/docs/openapi/typescript/clients.mdx +++ b/web/src/content/docs/docs/openapi/typescript/clients.mdx @@ -22,20 +22,20 @@ 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. -- [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..f2a88affbd 100644 --- a/web/src/content/docs/docs/openapi/typescript/core.mdx +++ b/web/src/content/docs/docs/openapi/typescript/core.mdx @@ -11,15 +11,15 @@ 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. -- [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). +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/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..3de9cc14c4 100644 --- a/web/src/content/docs/docs/openapi/typescript/mocks.mdx +++ b/web/src/content/docs/docs/openapi/typescript/mocks.mdx @@ -12,17 +12,17 @@ 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. -- [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). +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/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..b0920243a8 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 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. @@ -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..14183e8a5b 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 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. @@ -320,7 +320,47 @@ 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 + +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 @@ -557,7 +597,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 a410c8e2a5..08898cc501 100644 --- a/web/src/content/docs/docs/openapi/typescript/plugins/transformers.mdx +++ b/web/src/content/docs/docs/openapi/typescript/plugins/transformers.mdx @@ -7,14 +7,16 @@ import { Tabs, TabItem } from '@astrojs/starlight/components'; # Transformers +:::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. + +Otherwise, `@hey-api/transformers` is the right choice. +::: + 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. @@ -31,7 +33,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 { @@ -46,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 2414ed6e11..4f140a262f 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 { @@ -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 @@ -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..b842f53fb0 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`. +Valibot can validate or transform data directly in your SDK. Set `validator` or `transformer` to `true` on the SDK plugin to enable it. -```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 covers [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers) in more detail. ## 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..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 @@ -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`. +Zod can validate or transform data directly in your SDK. Set `validator` or `transformer` to `true` on the SDK plugin to enable it. -```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 covers [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers) in more detail. ## 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..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 @@ -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`. +Zod can validate or transform data directly in your SDK. Set `validator` or `transformer` to `true` on the SDK plugin to enable it. -```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 covers [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers) in more detail. ## 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 db48a0c806..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 @@ -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`. +Zod can validate or transform data directly in your SDK. Set `validator` or `transformer` to `true` on the SDK plugin to enable it. -```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 + 'zod', + { + 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 covers [validators](/docs/openapi/typescript/plugins/sdk#validators) and [transformers](/docs/openapi/typescript/plugins/sdk#transformers) in more detail. ## Output @@ -333,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..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,15 +12,15 @@ 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. -- [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). +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 e0f8a9325d..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,25 +12,27 @@ 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. -- [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). +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 a684bfab2b..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,20 +12,20 @@ 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. -- [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) - -Don't see your framework? Let us know your interest by [opening an issue](https://github.com/hey-api/openapi-ts/issues). +- [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 by [opening an issue](https://github.com/hey-api/openapi-ts/issues).