diff --git a/packages/client-axios/src/index.ts b/packages/client-axios/src/index.ts index 55c4970c8..3a377c8ba 100644 --- a/packages/client-axios/src/index.ts +++ b/packages/client-axios/src/index.ts @@ -64,8 +64,14 @@ export const createClient = (config: Config): Client => { let { data } = response; - if (opts.responseType === 'json' && opts.responseTransformer) { - data = await opts.responseTransformer(data); + if (opts.responseType === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } } return { diff --git a/packages/client-axios/src/types.ts b/packages/client-axios/src/types.ts index 56cacb235..927d791f5 100644 --- a/packages/client-axios/src/types.ts +++ b/packages/client-axios/src/types.ts @@ -83,11 +83,16 @@ export interface Config */ querySerializer?: QuerySerializer | QuerySerializerOptions; /** - * A function for transforming response data before it's returned to the - * caller function. This is an ideal place to post-process server data, - * e.g. convert date ISO strings into native Date objects. + * 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; /** * Throw an error instead of returning it in the response? * @@ -97,7 +102,7 @@ export interface Config } export interface RequestOptions< - ThrowOnError extends boolean = false, + ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config { /** diff --git a/packages/client-fetch/src/index.ts b/packages/client-fetch/src/index.ts index 2883b08cc..1985110ee 100644 --- a/packages/client-fetch/src/index.ts +++ b/packages/client-fetch/src/index.ts @@ -106,8 +106,14 @@ export const createClient = (config: Config = {}): Client => { : opts.parseAs) ?? 'json'; let data = await response[parseAs](); - if (parseAs === 'json' && opts.responseTransformer) { - data = await opts.responseTransformer(data); + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } } return { diff --git a/packages/client-fetch/src/types.ts b/packages/client-fetch/src/types.ts index 0c914db6c..4049d690b 100644 --- a/packages/client-fetch/src/types.ts +++ b/packages/client-fetch/src/types.ts @@ -88,11 +88,16 @@ export interface Config */ querySerializer?: QuerySerializer | QuerySerializerOptions; /** - * A function for transforming response data before it's returned to the - * caller function. This is an ideal place to post-process server data, - * e.g. convert date ISO strings into native Date objects. + * 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; /** * Throw an error instead of returning it in the response? * diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index ceb391a2a..cf8ae8215 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -11,7 +11,11 @@ import type { IRContext } from './ir/context'; import { parseExperimental, parseLegacy } from './openApi'; import type { ClientPlugins, UserPlugins } from './plugins'; import { defaultPluginConfigs } from './plugins'; -import type { DefaultPluginConfigs, PluginNames } from './plugins/types'; +import type { + DefaultPluginConfigs, + PluginContext, + PluginNames, +} from './plugins/types'; import type { Client } from './types/client'; import type { ClientConfig, @@ -180,44 +184,88 @@ const getOutput = (userConfig: ClientConfig): Config['output'] => { return output; }; -const getPluginOrder = ({ +const getPluginsConfig = ({ pluginConfigs, userPlugins, + userPluginsConfig, }: { pluginConfigs: DefaultPluginConfigs; userPlugins: ReadonlyArray; -}): Config['pluginOrder'] => { + userPluginsConfig: Config['plugins']; +}): Pick => { const circularReferenceTracker = new Set(); - const visitedNodes = new Set(); + const pluginOrder = new Set(); + const plugins: Config['plugins'] = {}; const dfs = (name: PluginNames) => { if (circularReferenceTracker.has(name)) { throw new Error(`Circular reference detected at '${name}'`); } - if (!visitedNodes.has(name)) { + if (!pluginOrder.has(name)) { circularReferenceTracker.add(name); const pluginConfig = pluginConfigs[name]; - if (!pluginConfig) { throw new Error( `🚫 unknown plugin dependency "${name}" - do you need to register a custom plugin with this name?`, ); } - for (const dependency of pluginConfig._dependencies || []) { - dfs(dependency); + const defaultOptions = defaultPluginConfigs[name]; + const userOptions = userPluginsConfig[name]; + if (userOptions && defaultOptions) { + const nativePluginOption = Object.keys(userOptions).find((key) => + key.startsWith('_'), + ); + if (nativePluginOption) { + throw new Error( + `🚫 cannot register plugin "${name}" - attempting to override a native plugin option "${nativePluginOption}"`, + ); + } } - for (const dependency of pluginConfig._optionalDependencies || []) { - if (userPlugins.includes(dependency)) { - dfs(dependency); - } + const config = { + _dependencies: [], + ...defaultOptions, + ...userOptions, + }; + + if (config._infer) { + const context: PluginContext = { + ensureDependency: (dependency) => { + if ( + typeof dependency === 'string' && + !config._dependencies.includes(dependency) + ) { + config._dependencies = [...config._dependencies, dependency]; + } + }, + pluginByTag: (tag) => { + for (const userPlugin of userPlugins) { + const defaultConfig = defaultPluginConfigs[userPlugin]; + if ( + defaultConfig && + defaultConfig._tags?.includes(tag) && + userPlugin !== name + ) { + return userPlugin; + } + } + }, + }; + config._infer(config, context); + } + + for (const dependency of config._dependencies) { + dfs(dependency); } circularReferenceTracker.delete(name); - visitedNodes.add(name); + pluginOrder.add(name); + + // @ts-expect-error + plugins[name] = config; } }; @@ -225,7 +273,10 @@ const getPluginOrder = ({ dfs(name); } - return Array.from(visitedNodes); + return { + pluginOrder: Array.from(pluginOrder), + plugins, + }; }; const getPlugins = ( @@ -248,42 +299,14 @@ const getPlugins = ( }) .filter(Boolean); - const pluginOrder = getPluginOrder({ + return getPluginsConfig({ pluginConfigs: { ...userPluginsConfig, ...defaultPluginConfigs, }, userPlugins, + userPluginsConfig, }); - - const plugins = pluginOrder.reduce( - (result, name) => { - const defaultOptions = defaultPluginConfigs[name]; - const userOptions = userPluginsConfig[name]; - if (userOptions && defaultOptions) { - const nativePluginOption = Object.keys(userOptions).find((key) => - key.startsWith('_'), - ); - if (nativePluginOption) { - throw new Error( - `🚫 cannot register plugin "${userOptions.name}" - attempting to override a native plugin option "${nativePluginOption}"`, - ); - } - } - // @ts-expect-error - result[name] = { - ...defaultOptions, - ...userOptions, - }; - return result; - }, - {} as Config['plugins'], - ); - - return { - pluginOrder, - plugins, - }; }; const getSpec = async ({ config }: { config: Config }) => { diff --git a/packages/openapi-ts/src/ir/operation.ts b/packages/openapi-ts/src/ir/operation.ts index 8be67d406..26dfde777 100644 --- a/packages/openapi-ts/src/ir/operation.ts +++ b/packages/openapi-ts/src/ir/operation.ts @@ -85,9 +85,21 @@ export const statusCodeToGroup = ({ }; interface OperationResponsesMap { + /** + * A deduplicated union of all error types. Unknown types are omitted. + */ error?: IRSchemaObject; + /** + * An object containing a map of status codes for each error type. + */ errors?: IRSchemaObject; + /** + * A deduplicated union of all response types. Unknown types are omitted. + */ response?: IRSchemaObject; + /** + * An object containing a map of status codes for each response type. + */ responses?: IRSchemaObject; } 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 ca0f51c2a..ca0acfa77 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts @@ -7,7 +7,27 @@ export const defaultConfig: Plugin.Config = { _dependencies: ['@hey-api/typescript'], _handler: handler, _handlerLegacy: handlerLegacy, - _optionalDependencies: ['@hey-api/transformers'], + _infer: (config, context) => { + if (config.transformer) { + if (typeof config.transformer === 'boolean') { + config.transformer = context.pluginByTag( + 'transformer', + ) as unknown as typeof config.transformer; + } + + context.ensureDependency(config.transformer); + } + + if (config.validator) { + if (typeof config.validator === 'boolean') { + config.validator = context.pluginByTag( + 'validator', + ) as unknown as typeof config.validator; + } + + context.ensureDependency(config.validator); + } + }, asClass: false, auth: true, name: '@hey-api/sdk', diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts index d59474702..faec0a5d8 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts @@ -11,55 +11,17 @@ import { } from '../../../ir/operation'; import { escapeComment } from '../../../utils/escape'; import { getServiceName } from '../../../utils/postprocess'; -import { irRef } from '../../../utils/ref'; -import { stringCase } from '../../../utils/stringCase'; import { transformServiceName } from '../../../utils/transform'; +import { operationIrRef } from '../../shared/utils/ref'; import type { Plugin } from '../../types'; -import { operationTransformerIrRef } from '../transformers/plugin'; +import { zodId } from '../../zod/plugin'; +import { + operationTransformerIrRef, + transformersId, +} from '../transformers/plugin'; import { serviceFunctionIdentifier } from './plugin-legacy'; import type { Config } from './types'; -interface OperationIRRef { - /** - * Operation ID - */ - id: string; -} - -export const operationIrRef = ({ - id, - type, -}: OperationIRRef & { - type: 'data' | 'error' | 'errors' | 'response' | 'responses'; -}): string => { - let affix = ''; - switch (type) { - case 'data': - affix = 'Data'; - break; - case 'error': - // error union - affix = 'Error'; - break; - case 'errors': - // errors map - affix = 'Errors'; - break; - case 'response': - // response union - affix = 'Response'; - break; - case 'responses': - // responses map - affix = 'Responses'; - break; - } - return `${irRef}${stringCase({ - case: 'PascalCase', - value: id, - })}-${affix}`; -}; - export const operationOptionsType = ({ importedType, throwOnError, @@ -322,20 +284,74 @@ const operationStatements = ({ } } - const fileTransformers = context.file({ id: 'transformers' }); - if (fileTransformers) { - const identifier = fileTransformers.identifier({ - $ref: operationTransformerIrRef({ id: operation.id, type: 'response' }), + if (plugin.transformer === '@hey-api/transformers') { + const identifierTransformer = context + .file({ id: transformersId })! + .identifier({ + $ref: operationTransformerIrRef({ id: operation.id, type: 'response' }), + namespace: 'value', + }); + + if (identifierTransformer.name) { + file.import({ + module: file.relativePathToFile({ + context, + id: transformersId, + }), + name: identifierTransformer.name, + }); + + requestOptions.push({ + key: 'responseTransformer', + value: identifierTransformer.name, + }); + } + } + + if (plugin.validator === 'zod') { + const identifierSchema = context.file({ id: zodId })!.identifier({ + $ref: operationIrRef({ + case: 'camelCase', + id: operation.id, + type: 'response', + }), namespace: 'value', }); - if (identifier.name) { + + if (identifierSchema.name) { file.import({ - module: file.relativePathToFile({ context, id: 'transformers' }), - name: identifier.name, + module: file.relativePathToFile({ + context, + id: zodId, + }), + name: identifierSchema.name, }); + requestOptions.push({ - key: 'responseTransformer', - value: identifier.name, + key: 'responseValidator', + value: compiler.arrowFunction({ + async: true, + parameters: [ + { + name: 'data', + }, + ], + statements: [ + compiler.returnStatement({ + expression: compiler.awaitExpression({ + expression: compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: compiler.identifier({ + text: identifierSchema.name, + }), + name: compiler.identifier({ text: 'parseAsync' }), + }), + parameters: [compiler.identifier({ text: 'data' })], + }), + }), + }), + ], + }), }); } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts index b2de7af43..406dde3c6 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts @@ -77,4 +77,32 @@ export interface Config extends Plugin.Name<'@hey-api/sdk'> { * @default '{{name}}Service' */ serviceNameBuilder?: string; + /** + * 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?: '@hey-api/transformers' | boolean; + /** + * **This feature works only with the experimental parser** + * + * Validate response data against schema before returning. This is useful + * if you want to ensure the response conforms to a desired shape. However, + * validation adds runtime overhead, so it's not recommended to use unless + * absolutely necessary. + * + * Ensure you have declared the selected library as a dependency to avoid + * errors. You can customize the selected validator output through its + * plugin. You can also set `validator` to `true` to automatically choose + * the validator from your defined plugins. + * + * @default false + */ + validator?: 'zod' | boolean; } diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/config.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/config.ts index e5d980d6e..3ec63394d 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/config.ts @@ -7,6 +7,7 @@ export const defaultConfig: Plugin.Config = { _dependencies: ['@hey-api/typescript'], _handler: handler, _handlerLegacy: handlerLegacy, + _tags: ['transformer'], dates: true, name: '@hey-api/transformers', output: 'transformers', diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts index 647e90b12..49f84f031 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts @@ -6,8 +6,8 @@ import type { IRSchemaObject } from '../../../ir/ir'; import { operationResponsesMap } from '../../../ir/operation'; import { irRef } from '../../../utils/ref'; import { stringCase } from '../../../utils/stringCase'; +import { operationIrRef } from '../../shared/utils/ref'; import type { Plugin } from '../../types'; -import { operationIrRef } from '../sdk/plugin'; import type { Config } from './types'; interface OperationIRRef { @@ -68,7 +68,7 @@ export const schemaResponseTransformerRef = ({ $ref: string; }): string => schemaIrRef({ $ref, type: 'response' }); -const transformersId = 'transformers'; +export const transformersId = 'transformers'; const dataVariableName = 'data'; const ensureStatements = ( diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts index 65d3918ca..0a5655028 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts @@ -15,8 +15,8 @@ import { irRef, isRefOpenApiComponent } from '../../../utils/ref'; import { digitsRegExp } from '../../../utils/regexp'; import { stringCase } from '../../../utils/stringCase'; import { fieldName } from '../../shared/utils/case'; +import { operationIrRef } from '../../shared/utils/ref'; import type { Plugin } from '../../types'; -import { operationIrRef } from '../sdk/plugin'; import type { Config } from './types'; interface SchemaWithType['type']> diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts index 0f3b3cd8c..b51e190f7 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts @@ -16,12 +16,10 @@ import { import { getConfig } from '../../../utils/config'; import { getServiceName } from '../../../utils/postprocess'; import { transformServiceName } from '../../../utils/transform'; -import { - operationIrRef, - operationOptionsType, -} from '../../@hey-api/sdk/plugin'; +import { operationOptionsType } from '../../@hey-api/sdk/plugin'; import { serviceFunctionIdentifier } from '../../@hey-api/sdk/plugin-legacy'; import { schemaToType } from '../../@hey-api/typescript/plugin'; +import { operationIrRef } from '../../shared/utils/ref'; import type { Plugin } from '../../types'; import type { Config as AngularQueryConfig } from '../angular-query-experimental'; import type { Config as ReactQueryConfig } from '../react-query'; diff --git a/packages/openapi-ts/src/plugins/fastify/plugin.ts b/packages/openapi-ts/src/plugins/fastify/plugin.ts index 56d66b960..64d0d5ae1 100644 --- a/packages/openapi-ts/src/plugins/fastify/plugin.ts +++ b/packages/openapi-ts/src/plugins/fastify/plugin.ts @@ -5,7 +5,7 @@ import type { IRContext } from '../../ir/context'; import type { IROperationObject } from '../../ir/ir'; import { operationResponsesMap } from '../../ir/operation'; import { hasParameterGroupObjectRequired } from '../../ir/parameter'; -import { operationIrRef } from '../@hey-api/sdk/plugin'; +import { operationIrRef } from '../shared/utils/ref'; import type { Plugin } from '../types'; import type { Config } from './types'; diff --git a/packages/openapi-ts/src/plugins/shared/utils/ref.ts b/packages/openapi-ts/src/plugins/shared/utils/ref.ts new file mode 100644 index 000000000..59a9788b6 --- /dev/null +++ b/packages/openapi-ts/src/plugins/shared/utils/ref.ts @@ -0,0 +1,46 @@ +import type { StringCase } from '../../../types/config'; +import { irRef } from '../../../utils/ref'; +import { stringCase } from '../../../utils/stringCase'; + +interface OperationIRRef { + /** + * Operation ID + */ + id: string; +} + +export const operationIrRef = ({ + case: _case = 'PascalCase', + id, + type, +}: OperationIRRef & { + readonly case?: StringCase; + type: 'data' | 'error' | 'errors' | 'response' | 'responses'; +}): string => { + let affix = ''; + switch (type) { + case 'data': + affix = 'Data'; + break; + case 'error': + // error union + affix = 'Error'; + break; + case 'errors': + // errors map + affix = 'Errors'; + break; + case 'response': + // response union + affix = 'Response'; + break; + case 'responses': + // responses map + affix = 'Responses'; + break; + } + return `${irRef}${stringCase({ + case: _case, + value: id, + })}-${affix}`; +}; diff --git a/packages/openapi-ts/src/plugins/types.d.ts b/packages/openapi-ts/src/plugins/types.d.ts index c4ca5a357..e818f83cb 100644 --- a/packages/openapi-ts/src/plugins/types.d.ts +++ b/packages/openapi-ts/src/plugins/types.d.ts @@ -3,6 +3,10 @@ import type { OpenApi } from '../openApi'; import type { Client } from '../types/client'; import type { Files } from '../types/utils'; +type OmitUnderscoreKeys = { + [K in keyof T as K extends `_${string}` ? never : K]: T[K]; +}; + export type PluginNames = | '@hey-api/schemas' | '@hey-api/sdk' @@ -16,28 +20,44 @@ export type PluginNames = | 'fastify' | 'zod'; +type PluginTag = 'transformer' | 'validator'; + +export interface PluginContext { + ensureDependency: (name: PluginNames | true) => void; + pluginByTag: (tag: PluginTag) => PluginNames | undefined; +} + interface BaseConfig { // eslint-disable-next-line @typescript-eslint/ban-types name: PluginNames | (string & {}); output?: string; } -interface Dependencies { +interface Meta { /** - * Required dependencies will be always processed, regardless of whether - * a user defines them in their `plugins` config. + * Dependency plugins will be always processed, regardless of whether user + * explicitly defines them in their `plugins` config. */ _dependencies?: ReadonlyArray; /** - * Optional dependencies are not processed unless a user explicitly defines - * them in their `plugins` config. + * Allows overriding config before it's sent to the parser. An example is + * defining `validator` as `true` and the plugin figures out which plugin + * should be used for validation. + */ + _infer?: ( + config: Config & Omit, '_infer'>, + context: PluginContext, + ) => void; + /** + * Optional tags can be used to help with deciding plugin order and inferring + * plugin configuration options. */ - _optionalDependencies?: ReadonlyArray; + _tags?: ReadonlyArray; } export type DefaultPluginConfigs = { [K in PluginNames]: BaseConfig & - Dependencies & { + Meta & { _handler: Plugin.Handler>>; _handlerLegacy: Plugin.LegacyHandler>>; }; @@ -48,7 +68,7 @@ export type DefaultPluginConfigs = { */ export namespace Plugin { export type Config = Config & - Dependencies & { + Meta & { _handler: Plugin.Handler; _handlerLegacy: Plugin.LegacyHandler; }; @@ -65,10 +85,7 @@ export namespace Plugin { plugin: Plugin.Instance; }) => void; - export type Instance = Omit< - Config, - '_dependencies' | '_handler' | '_handlerLegacy' | '_optionalDependencies' - > & + export type Instance = OmitUnderscoreKeys & Pick, 'output'>; /** diff --git a/packages/openapi-ts/src/plugins/zod/config.ts b/packages/openapi-ts/src/plugins/zod/config.ts index 3dc397675..d5e95589b 100644 --- a/packages/openapi-ts/src/plugins/zod/config.ts +++ b/packages/openapi-ts/src/plugins/zod/config.ts @@ -5,6 +5,7 @@ import type { Config } from './types'; export const defaultConfig: Plugin.Config = { _handler: handler, _handlerLegacy: () => {}, + _tags: ['validator'], name: 'zod', output: 'zod', }; diff --git a/packages/openapi-ts/src/plugins/zod/plugin.ts b/packages/openapi-ts/src/plugins/zod/plugin.ts index 75d934e26..ca52732d3 100644 --- a/packages/openapi-ts/src/plugins/zod/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/plugin.ts @@ -2,10 +2,11 @@ import ts from 'typescript'; import { compiler } from '../../compiler'; import type { IRContext } from '../../ir/context'; -import type { IRSchemaObject } from '../../ir/ir'; +import type { IROperationObject, IRSchemaObject } from '../../ir/ir'; +import { operationResponsesMap } from '../../ir/operation'; import { deduplicateSchema } from '../../ir/schema'; -import { isRefOpenApiComponent } from '../../utils/ref'; import { digitsRegExp } from '../../utils/regexp'; +import { operationIrRef } from '../shared/utils/ref'; import type { Plugin } from '../types'; import type { Config } from './types'; @@ -19,25 +20,26 @@ interface Result { hasCircularReference: boolean; } -const zodId = 'zod'; +export const zodId = 'zod'; // frequently used identifiers const defaultIdentifier = compiler.identifier({ text: 'default' }); +const intersectionIdentifier = compiler.identifier({ text: 'intersection' }); const lazyIdentifier = compiler.identifier({ text: 'lazy' }); +const mergeIdentifier = compiler.identifier({ text: 'merge' }); const optionalIdentifier = compiler.identifier({ text: 'optional' }); const readonlyIdentifier = compiler.identifier({ text: 'readonly' }); +const unionIdentifier = compiler.identifier({ text: 'union' }); const zIdentifier = compiler.identifier({ text: 'z' }); -const nameTransformer = (name: string) => `z${name}`; +const nameTransformer = (name: string) => `z-${name}`; const arrayTypeToZodSchema = ({ context, - namespace, result, schema, }: { context: IRContext; - namespace: Array; result: Result; schema: SchemaWithType<'array'>; }): ts.CallExpression => { @@ -54,7 +56,6 @@ const arrayTypeToZodSchema = ({ parameters: [ unknownTypeToZodSchema({ context, - namespace, schema: { type: 'unknown', }, @@ -68,7 +69,6 @@ const arrayTypeToZodSchema = ({ const itemExpressions = schema.items!.map((item) => schemaToZodSchema({ context, - namespace, result, schema: item, }), @@ -95,7 +95,6 @@ const arrayTypeToZodSchema = ({ parameters: [ unknownTypeToZodSchema({ context, - namespace, schema: { type: 'unknown', }, @@ -142,7 +141,6 @@ const booleanTypeToZodSchema = ({ schema, }: { context: IRContext; - namespace: Array; schema: SchemaWithType<'boolean'>; }) => { if (schema.const !== undefined) { @@ -163,11 +161,9 @@ const booleanTypeToZodSchema = ({ const enumTypeToZodSchema = ({ context, - namespace, schema, }: { context: IRContext; - namespace: Array; schema: SchemaWithType<'enum'>; }): ts.CallExpression => { const enumMembers: Array = []; @@ -186,7 +182,6 @@ const enumTypeToZodSchema = ({ if (!enumMembers.length) { return unknownTypeToZodSchema({ context, - namespace, schema: { type: 'unknown', }, @@ -213,7 +208,6 @@ const neverTypeToZodSchema = ({ schema, }: { context: IRContext; - namespace: Array; schema: SchemaWithType<'never'>; }) => { const expression = compiler.callExpression({ @@ -229,7 +223,6 @@ const nullTypeToZodSchema = ({ schema, }: { context: IRContext; - namespace: Array; schema: SchemaWithType<'null'>; }) => { const expression = compiler.callExpression({ @@ -245,7 +238,6 @@ const numberTypeToZodSchema = ({ schema, }: { context: IRContext; - namespace: Array; schema: SchemaWithType<'number'>; }) => { let numberExpression = compiler.callExpression({ @@ -307,12 +299,10 @@ const numberTypeToZodSchema = ({ const objectTypeToZodSchema = ({ context, - // namespace, result, schema, }: { context: IRContext; - namespace: Array; result: Result; schema: SchemaWithType<'object'>; }) => { @@ -413,7 +403,6 @@ const objectTypeToZodSchema = ({ // name: 'key', // type: schemaToZodSchema({ // context, - // namespace, // schema: // indexPropertyItems.length === 1 // ? indexPropertyItems[0] @@ -444,7 +433,6 @@ const stringTypeToZodSchema = ({ schema, }: { context: IRContext; - namespace: Array; schema: SchemaWithType<'string'>; }) => { let stringExpression = compiler.callExpression({ @@ -539,7 +527,6 @@ const undefinedTypeToZodSchema = ({ schema, }: { context: IRContext; - namespace: Array; schema: SchemaWithType<'undefined'>; }) => { const expression = compiler.callExpression({ @@ -555,7 +542,6 @@ const unknownTypeToZodSchema = ({ schema, }: { context: IRContext; - namespace: Array; schema: SchemaWithType<'unknown'>; }) => { const expression = compiler.callExpression({ @@ -571,7 +557,6 @@ const voidTypeToZodSchema = ({ schema, }: { context: IRContext; - namespace: Array; schema: SchemaWithType<'void'>; }) => { const expression = compiler.callExpression({ @@ -585,12 +570,10 @@ const voidTypeToZodSchema = ({ const schemaTypeToZodSchema = ({ context, - namespace, result, schema, }: { context: IRContext; - namespace: Array; result: Result; schema: IRSchemaObject; }): ts.Expression => { @@ -598,58 +581,49 @@ const schemaTypeToZodSchema = ({ case 'array': return arrayTypeToZodSchema({ context, - namespace, result, schema: schema as SchemaWithType<'array'>, }); case 'boolean': return booleanTypeToZodSchema({ context, - namespace, schema: schema as SchemaWithType<'boolean'>, }); case 'enum': return enumTypeToZodSchema({ context, - namespace, schema: schema as SchemaWithType<'enum'>, }); case 'never': return neverTypeToZodSchema({ context, - namespace, schema: schema as SchemaWithType<'never'>, }); case 'null': return nullTypeToZodSchema({ context, - namespace, schema: schema as SchemaWithType<'null'>, }); case 'number': return numberTypeToZodSchema({ context, - namespace, schema: schema as SchemaWithType<'number'>, }); case 'object': return objectTypeToZodSchema({ context, - namespace, result, schema: schema as SchemaWithType<'object'>, }); case 'string': return stringTypeToZodSchema({ context, - namespace, schema: schema as SchemaWithType<'string'>, }); case 'tuple': // TODO: parser - temporary unknown while not handled return unknownTypeToZodSchema({ context, - namespace, schema: { type: 'unknown', }, @@ -657,41 +631,64 @@ const schemaTypeToZodSchema = ({ // TODO: parser - handle tuple // return tupleTypeToIdentifier({ // context, - // namespace, // schema: schema as SchemaWithType<'tuple'>, // }); case 'undefined': return undefinedTypeToZodSchema({ context, - namespace, schema: schema as SchemaWithType<'undefined'>, }); case 'unknown': return unknownTypeToZodSchema({ context, - namespace, schema: schema as SchemaWithType<'unknown'>, }); case 'void': return voidTypeToZodSchema({ context, - namespace, schema: schema as SchemaWithType<'void'>, }); } }; +const operationToZodSchema = ({ + context, + operation, + result, +}: { + context: IRContext; + operation: IROperationObject; + result: Result; +}) => { + if (operation.responses) { + const { response } = operationResponsesMap(operation); + + if (response) { + schemaToZodSchema({ + $ref: operationIrRef({ + case: 'camelCase', + id: operation.id, + type: 'response', + }), + context, + result, + schema: response, + }); + } + } +}; + const schemaToZodSchema = ({ $ref, context, - // TODO: parser - remove namespace, it's a type plugin construct - namespace = [], result, schema, }: { + /** + * When $ref is supplied, a node will be emitted to the file. + */ $ref?: string; context: IRContext; - namespace?: Array; result: Result; schema: IRSchemaObject; }): ts.Expression => { @@ -703,8 +700,7 @@ const schemaToZodSchema = ({ if ($ref) { result.circularReferenceTracker.add($ref); - // emit nodes only if $ref points to a reusable component - if (isRefOpenApiComponent($ref)) { + if ($ref) { identifier = file.identifier({ $ref, create: true, @@ -770,46 +766,73 @@ const schemaToZodSchema = ({ } else if (schema.type) { expression = schemaTypeToZodSchema({ context, - namespace, result, schema, }); } else if (schema.items) { - // TODO: parser - temporary unknown while not handled - expression = unknownTypeToZodSchema({ - context, - namespace, - schema: { - type: 'unknown', - }, - }); + schema = deduplicateSchema({ schema }); - // TODO: parser - handle items - // schema = deduplicateSchema({ schema }); - // if (schema.items) { - // const itemTypes = schema.items.map((item) => - // schemaToZodSchema({ - // context, - // namespace, - // schema: item, - // }), - // ); - // expression = - // schema.logicalOperator === 'and' - // ? compiler.typeIntersectionNode({ types: itemTypes }) - // : compiler.typeUnionNode({ types: itemTypes }); - // } else { - // expression = schemaToZodSchema({ - // context, - // namespace, - // schema, - // }); - // } + if (schema.items) { + const itemTypes = schema.items.map((item) => + schemaToZodSchema({ + context, + result, + schema: item, + }), + ); + + if (schema.logicalOperator === 'and') { + const firstSchema = schema.items[0]; + // we want to add an intersection, but not every schema can use the same API. + // if the first item contains another array or not an object, we cannot use + // `.merge()` as that does not exist on `.union()` and non-object schemas. + if ( + firstSchema.logicalOperator === 'or' || + (firstSchema.type && firstSchema.type !== 'object') + ) { + expression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: zIdentifier, + name: intersectionIdentifier, + }), + parameters: itemTypes, + }); + } else { + expression = itemTypes[0]; + itemTypes.slice(1).forEach((item) => { + expression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: expression!, + name: mergeIdentifier, + }), + parameters: [item], + }); + }); + } + } else { + expression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: zIdentifier, + name: unionIdentifier, + }), + parameters: [ + compiler.arrayLiteralExpression({ + elements: itemTypes, + }), + ], + }); + } + } else { + expression = schemaToZodSchema({ + context, + result, + schema, + }); + } } else { // catch-all fallback for failed schemas expression = schemaTypeToZodSchema({ context, - namespace, result, schema: { type: 'unknown', @@ -843,6 +866,7 @@ const schemaToZodSchema = ({ export const handler: Plugin.Handler = ({ context, plugin }) => { const file = context.createFile({ id: zodId, + identifierCase: 'camelCase', path: plugin.output, }); @@ -851,13 +875,18 @@ export const handler: Plugin.Handler = ({ context, plugin }) => { name: 'z', }); - // context.subscribe('operation', ({ operation }) => { - // schemaToZodSchema({ - // $ref, - // context, - // schema, - // }); - // }); + context.subscribe('operation', ({ operation }) => { + const result: Result = { + circularReferenceTracker: new Set(), + hasCircularReference: false, + }; + + operationToZodSchema({ + context, + operation, + result, + }); + }); context.subscribe('schema', ({ $ref, schema }) => { const result: Result = { diff --git a/packages/openapi-ts/test/__snapshots__/3.0.x/plugins/zod/default/zod.gen.ts b/packages/openapi-ts/test/__snapshots__/3.0.x/plugins/zod/default/zod.gen.ts index b0eac6bf3..90a7326d2 100644 --- a/packages/openapi-ts/test/__snapshots__/3.0.x/plugins/zod/default/zod.gen.ts +++ b/packages/openapi-ts/test/__snapshots__/3.0.x/plugins/zod/default/zod.gen.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; export const z400 = z.string(); -export const zcamelCaseCommentWithBreaks = z.number(); +export const zCamelCaseCommentWithBreaks = z.number(); export const zCommentWithBreaks = z.number(); @@ -26,7 +26,7 @@ export const zSimpleBoolean = z.boolean(); export const zSimpleString = z.string(); -export const zNonAsciiStringæøåÆØÅöôêÊ字符串 = z.string(); +export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); export const zSimpleFile = z.string(); @@ -34,7 +34,10 @@ export const zSimpleReference = z.object({ prop: z.string().optional() }); -export const zSimpleStringWithPattern = z.unknown(); +export const zSimpleStringWithPattern = z.union([ + z.string().max(64), + z.null() +]); export const zEnumWithStrings = z.enum([ 'Success', @@ -75,7 +78,7 @@ export const zArrayWithArray = z.array(z.array(z.object({ }))); export const zArrayWithProperties = z.array(z.object({ - '16x16': zcamelCaseCommentWithBreaks.optional(), + '16x16': zCamelCaseCommentWithBreaks.optional(), bar: z.string().optional() })); @@ -120,13 +123,25 @@ export const zModelWithStringError = z.object({ prop: z.string().optional() }); -export const zModel_From_Zendesk = z.string(); +export const zModelFromZendesk = z.string(); export const zModelWithNullableString = z.object({ - nullableProp1: z.unknown().optional(), - nullableRequiredProp1: z.unknown(), - nullableProp2: z.unknown().optional(), - nullableRequiredProp2: z.unknown(), + nullableProp1: z.union([ + z.string(), + z.null() + ]).optional(), + nullableRequiredProp1: z.union([ + z.string(), + z.null() + ]), + nullableProp2: z.union([ + z.string(), + z.null() + ]).optional(), + nullableRequiredProp2: z.union([ + z.string(), + z.null() + ]), 'foo_bar-enum': z.enum([ 'Success', 'Warning', @@ -184,7 +199,10 @@ export const zModelWithReference = z.object({ prop: z.object({ required: z.string(), requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.unknown(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), string: z.string().optional(), number: z.number().optional(), boolean: z.boolean().optional(), @@ -221,12 +239,29 @@ export const zDeprecatedModel = z.object({ prop: z.string().optional() }); +export const zModelWithCircularReference: z.ZodTypeAny = z.object({ + prop: z.lazy(() => { + return zModelWithCircularReference; + }).optional() +}); + export const zCompositionWithOneOf = z.object({ - propA: z.unknown().optional() + propA: z.union([ + zModelWithString, + zModelWithEnum, + zModelWithArray, + zModelWithDictionary + ]).optional() }); export const zCompositionWithOneOfAnonymous = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.object({ + propA: z.string().optional() + }), + z.string(), + z.number() + ]).optional() }); export const zModelCircle = z.object({ @@ -239,21 +274,48 @@ export const zModelSquare = z.object({ sideLength: z.number().optional() }); -export const zCompositionWithOneOfDiscriminator = z.unknown(); +export const zCompositionWithOneOfDiscriminator = z.union([ + z.object({ + kind: z.string().optional() + }).merge(zModelCircle), + z.object({ + kind: z.string().optional() + }).merge(zModelSquare) +]); export const zCompositionWithAnyOf = z.object({ - propA: z.unknown().optional() + propA: z.union([ + zModelWithString, + zModelWithEnum, + zModelWithArray, + zModelWithDictionary + ]).optional() }); export const zCompositionWithAnyOfAnonymous = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.object({ + propA: z.string().optional() + }), + z.string(), + z.number() + ]).optional() }); export const zCompositionWithNestedAnyAndTypeNull = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.array(z.union([ + zModelWithDictionary, + z.null() + ])), + z.array(z.union([ + zModelWithArray, + z.null() + ])) + ]).optional() }); -export const z3e_num_1Период = z.enum([ +export const z3eNum1Период = z.enum([ 'Bird', 'Dog' ]); @@ -263,31 +325,64 @@ export const zConstValue = z.enum([ ]); export const zCompositionWithNestedAnyOfAndNull = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.array(z.unknown()), + z.null() + ]).optional() }); export const zCompositionWithOneOfAndNullable = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.object({ + boolean: z.boolean().optional() + }), + zModelWithEnum, + zModelWithArray, + zModelWithDictionary, + z.null() + ]).optional() }); export const zCompositionWithOneOfAndSimpleDictionary = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.boolean(), + z.object({}) + ]).optional() }); export const zCompositionWithOneOfAndSimpleArrayDictionary = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.boolean(), + z.object({}) + ]).optional() }); export const zCompositionWithOneOfAndComplexArrayDictionary = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.boolean(), + z.object({}) + ]).optional() }); export const zCompositionWithAllOfAndNullable = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.object({ + boolean: z.boolean().optional() + }).merge(zModelWithEnum).merge(zModelWithArray).merge(zModelWithDictionary), + z.null() + ]).optional() }); export const zCompositionWithAnyOfAndNullable = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.object({ + boolean: z.boolean().optional() + }), + zModelWithEnum, + zModelWithArray, + zModelWithDictionary, + z.null() + ]).optional() }); export const zCompositionBaseModel = z.object({ @@ -295,12 +390,19 @@ export const zCompositionBaseModel = z.object({ lastname: z.string().optional() }); -export const zCompositionExtendedModel = z.unknown(); +export const zCompositionExtendedModel = zCompositionBaseModel.merge(z.object({ + age: z.number(), + firstName: z.string(), + lastname: z.string() +})); export const zModelWithProperties = z.object({ required: z.string(), requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.unknown(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), string: z.string().optional(), number: z.number().optional(), boolean: z.boolean().optional(), @@ -313,7 +415,20 @@ export const zModelWithProperties = z.object({ }); export const zModelWithNestedProperties = z.object({ - first: z.unknown().readonly() + first: z.union([ + z.object({ + second: z.union([ + z.object({ + third: z.union([ + z.string(), + z.null() + ]).readonly() + }), + z.null() + ]).readonly() + }), + z.null() + ]).readonly() }); export const zModelWithDuplicateProperties = z.object({ @@ -332,9 +447,15 @@ export const zModelWithDuplicateImports = z.object({ propC: zModelWithString.optional() }); -export const zModelThatExtends = z.unknown(); +export const zModelThatExtends = zModelWithString.merge(z.object({ + propExtendsA: z.string().optional(), + propExtendsB: zModelWithString.optional() +})); -export const zModelThatExtendsExtends = z.unknown(); +export const zModelThatExtendsExtends = zModelWithString.merge(zModelThatExtends).merge(z.object({ + propExtendsC: z.string().optional(), + propExtendsD: zModelWithString.optional() +})); export const zModelWithPattern = z.object({ key: z.string().max(64), @@ -356,7 +477,7 @@ export const zFile = z.object({ file: z.string().url().readonly().optional() }); -export const zdefault = z.object({ +export const zDefault = z.object({ name: z.string().optional() }); @@ -388,12 +509,33 @@ export const zModelWithAdditionalPropertiesEqTrue = z.object({ }); export const zNestedAnyOfArraysNullable = z.object({ - nullableArray: z.unknown().optional() + nullableArray: z.union([ + z.array(z.unknown()), + z.null() + ]).optional() }); -export const zCompositionWithOneOfAndProperties = z.unknown(); +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: z.unknown() + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.number().gte(0), + z.null() + ]), + qux: z.number().gte(0) +})); -export const zNullableObject = z.unknown(); +export const zNullableObject = z.union([ + z.object({ + foo: z.string().optional() + }), + z.null() +]); export const zCharactersInDescription = z.string(); @@ -401,7 +543,35 @@ export const zModelWithNullableObject = z.object({ data: zNullableObject.optional() }); -export const zModelWithOneOfEnum = z.unknown(); +export const zModelWithOneOfEnum = z.union([ + z.object({ + foo: z.enum([ + 'Bar' + ]) + }), + z.object({ + foo: z.enum([ + 'Baz' + ]) + }), + z.object({ + foo: z.enum([ + 'Qux' + ]) + }), + z.object({ + content: z.string().datetime(), + foo: z.enum([ + 'Quux' + ]) + }), + z.object({ + content: z.unknown(), + foo: z.enum([ + 'Corge' + ]) + }) +]); export const zModelWithNestedArrayEnumsDataFoo = z.enum([ 'foo', @@ -453,7 +623,16 @@ export const zModelWithBackticksInDescription = z.object({ template: z.string().optional() }); -export const zModelWithOneOfAndProperties = z.unknown(); +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + z.unknown(), + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.number().gte(0), + z.null() + ]), + qux: z.number().gte(0) +})); export const zParameterSimpleParameterUnused = z.string(); @@ -465,7 +644,7 @@ export const zDeleteFooData = z.string(); export const zDeleteFooData2 = z.string(); -export const zimport = z.string(); +export const zImport = z.string(); export const zSchemaWithFormRestrictedKeys = z.object({ description: z.string().optional(), @@ -489,14 +668,14 @@ export const zSchemaWithFormRestrictedKeys = z.object({ })).optional() }); -export const zio_k8s_apimachinery_pkg_apis_meta_v1_DeleteOptions = z.object({ +export const zIoK8sApimachineryPkgApisMetaV1DeleteOptions = z.object({ preconditions: z.object({ resourceVersion: z.string().optional(), uid: z.string().optional() }).optional() }); -export const zio_k8s_apimachinery_pkg_apis_meta_v1_Preconditions = z.object({ +export const zIoK8sApimachineryPkgApisMetaV1Preconditions = z.object({ resourceVersion: z.string().optional(), uid: z.string().optional() }); @@ -505,23 +684,125 @@ export const zAdditionalPropertiesUnknownIssue = z.object({}); export const zAdditionalPropertiesUnknownIssue2 = z.object({}); -export const zAdditionalPropertiesUnknownIssue3 = z.unknown(); +export const zAdditionalPropertiesUnknownIssue3 = z.intersection(z.string(), z.object({ + entries: z.object({}) +})); export const zAdditionalPropertiesIntegerIssue = z.object({ value: z.number() }); -export const zOneOfAllOfIssue = z.unknown(); +export const zOneOfAllOfIssue = z.union([ + z.intersection(z.union([ + zConstValue, + z.object({ + item: z.boolean().optional(), + error: z.union([ + z.string(), + z.null() + ]).optional(), + hasError: z.boolean().readonly().optional(), + data: z.object({}).optional() + }) + ]), z3eNum1Период), + z.object({ + item: z.union([ + z.string(), + z.null() + ]).optional(), + error: z.union([ + z.string(), + z.null() + ]).optional(), + hasError: z.boolean().readonly().optional() + }) +]); -export const zGeneric_Schema_Duplicate_Issue_1_System_Boolean_ = z.object({ +export const zGenericSchemaDuplicateIssue1SystemBoolean = z.object({ item: z.boolean().optional(), - error: z.unknown().optional(), + error: z.union([ + z.string(), + z.null() + ]).optional(), hasError: z.boolean().readonly().optional(), data: z.object({}).optional() }); -export const zGeneric_Schema_Duplicate_Issue_1_System_String_ = z.object({ - item: z.unknown().optional(), - error: z.unknown().optional(), +export const zGenericSchemaDuplicateIssue1SystemString = z.object({ + item: z.union([ + z.string(), + z.null() + ]).optional(), + error: z.union([ + z.string(), + z.null() + ]).optional(), hasError: z.boolean().readonly().optional() -}); \ No newline at end of file +}); + +export const zImportResponse = z.union([ + zModelFromZendesk, + zModelWithReadOnlyAndWriteOnly +]); + +export const zApiVVersionODataControllerCountResponse = zModelFromZendesk; + +export const zGetApiVbyApiVersionSimpleOperationResponse = z.number(); + +export const zPostCallWithOptionalParamResponse = z.union([ + z.number(), + z.void() +]); + +export const zCallWithNoContentResponseResponse = z.void(); + +export const zCallWithResponseAndNoContentResponseResponse = z.union([ + z.number(), + z.void() +]); + +export const zDummyAResponse = z400; + +export const zDummyBResponse = z.void(); + +export const zCallWithResponseResponse = zImport; + +export const zCallWithDuplicateResponsesResponse = z.union([ + zModelWithBoolean.merge(zModelWithInteger), + zModelWithString +]); + +export const zCallWithResponsesResponse = z.union([ + z.object({ + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().readonly().optional(), + value: z.array(zModelWithString).readonly().optional() + }), + zModelThatExtends, + zModelThatExtendsExtends +]); + +export const zTypesResponse = z.union([ + z.number(), + z.string(), + z.boolean(), + z.object({}) +]); + +export const zUploadFileResponse = z.boolean(); + +export const zFileResponseResponse = z.string(); + +export const zComplexTypesResponse = z.array(zModelWithString); + +export const zMultipartResponseResponse = z.object({ + file: z.string().optional(), + metadata: z.object({ + foo: z.string().optional(), + bar: z.string().optional() + }).optional() +}); + +export const zComplexParamsResponse = zModelWithString; + +export const zNonAsciiæøåÆøÅöôêÊ字符串Response = z.array(zNonAsciiStringæøåÆøÅöôêÊ字符串); \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/plugins/zod/default/zod.gen.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/plugins/zod/default/zod.gen.ts index 06ce5d746..ac499cb56 100644 --- a/packages/openapi-ts/test/__snapshots__/3.1.x/plugins/zod/default/zod.gen.ts +++ b/packages/openapi-ts/test/__snapshots__/3.1.x/plugins/zod/default/zod.gen.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; export const z400 = z.string(); -export const zcamelCaseCommentWithBreaks = z.number(); +export const zCamelCaseCommentWithBreaks = z.number(); export const zCommentWithBreaks = z.number(); @@ -26,7 +26,7 @@ export const zSimpleBoolean = z.boolean(); export const zSimpleString = z.string(); -export const zNonAsciiStringæøåÆØÅöôêÊ字符串 = z.string(); +export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); export const zSimpleFile = z.string(); @@ -34,7 +34,10 @@ export const zSimpleReference = z.object({ prop: z.string().optional() }); -export const zSimpleStringWithPattern = z.unknown(); +export const zSimpleStringWithPattern = z.union([ + z.string().max(64), + z.null() +]); export const zEnumWithStrings = z.enum([ 'Success', @@ -75,14 +78,17 @@ export const zArrayWithArray = z.array(z.array(z.object({ }))); export const zArrayWithProperties = z.array(z.object({ - '16x16': zcamelCaseCommentWithBreaks.optional(), + '16x16': zCamelCaseCommentWithBreaks.optional(), bar: z.string().optional() })); export const zArrayWithAnyOfProperties = z.array(z.unknown()); export const zAnyOfAnyAndNull = z.object({ - data: z.unknown().optional() + data: z.union([ + z.unknown(), + z.null() + ]).optional() }); export const zAnyOfArrays = z.object({ @@ -120,13 +126,25 @@ export const zModelWithStringError = z.object({ prop: z.string().optional() }); -export const zModel_From_Zendesk = z.string(); +export const zModelFromZendesk = z.string(); export const zModelWithNullableString = z.object({ - nullableProp1: z.unknown().optional(), - nullableRequiredProp1: z.unknown(), - nullableProp2: z.unknown().optional(), - nullableRequiredProp2: z.unknown(), + nullableProp1: z.union([ + z.string(), + z.null() + ]).optional(), + nullableRequiredProp1: z.union([ + z.string(), + z.null() + ]), + nullableProp2: z.union([ + z.string(), + z.null() + ]).optional(), + nullableRequiredProp2: z.union([ + z.string(), + z.null() + ]), 'foo_bar-enum': z.enum([ 'Success', 'Warning', @@ -184,7 +202,10 @@ export const zModelWithReference = z.object({ prop: z.object({ required: z.string(), requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.unknown(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), string: z.string().optional(), number: z.number().optional(), boolean: z.boolean().optional(), @@ -221,12 +242,29 @@ export const zDeprecatedModel = z.object({ prop: z.string().optional() }); +export const zModelWithCircularReference: z.ZodTypeAny = z.object({ + prop: z.lazy(() => { + return zModelWithCircularReference; + }).optional() +}); + export const zCompositionWithOneOf = z.object({ - propA: z.unknown().optional() + propA: z.union([ + zModelWithString, + zModelWithEnum, + zModelWithArray, + zModelWithDictionary + ]).optional() }); export const zCompositionWithOneOfAnonymous = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.object({ + propA: z.string().optional() + }), + z.string(), + z.number() + ]).optional() }); export const zModelCircle = z.object({ @@ -239,21 +277,42 @@ export const zModelSquare = z.object({ sideLength: z.number().optional() }); -export const zCompositionWithOneOfDiscriminator = z.unknown(); +export const zCompositionWithOneOfDiscriminator = z.union([ + z.object({ + kind: z.string().optional() + }).merge(zModelCircle), + z.object({ + kind: z.string().optional() + }).merge(zModelSquare) +]); export const zCompositionWithAnyOf = z.object({ - propA: z.unknown().optional() + propA: z.union([ + zModelWithString, + zModelWithEnum, + zModelWithArray, + zModelWithDictionary + ]).optional() }); export const zCompositionWithAnyOfAnonymous = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.object({ + propA: z.string().optional() + }), + z.string(), + z.number() + ]).optional() }); export const zCompositionWithNestedAnyAndTypeNull = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.array(z.unknown()), + z.array(z.unknown()) + ]).optional() }); -export const z3e_num_1Период = z.enum([ +export const z3eNum1Период = z.enum([ 'Bird', 'Dog' ]); @@ -261,31 +320,64 @@ export const z3e_num_1Период = z.enum([ export const zConstValue = z.string(); export const zCompositionWithNestedAnyOfAndNull = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.array(z.unknown()), + z.null() + ]).optional() }); export const zCompositionWithOneOfAndNullable = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.object({ + boolean: z.boolean().optional() + }), + zModelWithEnum, + zModelWithArray, + zModelWithDictionary, + z.null() + ]).optional() }); export const zCompositionWithOneOfAndSimpleDictionary = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.boolean(), + z.object({}) + ]).optional() }); export const zCompositionWithOneOfAndSimpleArrayDictionary = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.boolean(), + z.object({}) + ]).optional() }); export const zCompositionWithOneOfAndComplexArrayDictionary = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.boolean(), + z.object({}) + ]).optional() }); export const zCompositionWithAllOfAndNullable = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.object({ + boolean: z.boolean().optional() + }).merge(zModelWithEnum).merge(zModelWithArray).merge(zModelWithDictionary), + z.null() + ]).optional() }); export const zCompositionWithAnyOfAndNullable = z.object({ - propA: z.unknown().optional() + propA: z.union([ + z.object({ + boolean: z.boolean().optional() + }), + zModelWithEnum, + zModelWithArray, + zModelWithDictionary, + z.null() + ]).optional() }); export const zCompositionBaseModel = z.object({ @@ -293,12 +385,19 @@ export const zCompositionBaseModel = z.object({ lastname: z.string().optional() }); -export const zCompositionExtendedModel = z.unknown(); +export const zCompositionExtendedModel = zCompositionBaseModel.merge(z.object({ + age: z.number(), + firstName: z.string(), + lastname: z.string() +})); export const zModelWithProperties = z.object({ required: z.string(), requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.unknown(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), string: z.string().optional(), number: z.number().optional(), boolean: z.boolean().optional(), @@ -311,7 +410,20 @@ export const zModelWithProperties = z.object({ }); export const zModelWithNestedProperties = z.object({ - first: z.unknown().readonly() + first: z.union([ + z.object({ + second: z.union([ + z.object({ + third: z.union([ + z.string(), + z.null() + ]).readonly() + }), + z.null() + ]).readonly() + }), + z.null() + ]).readonly() }); export const zModelWithDuplicateProperties = z.object({ @@ -330,9 +442,15 @@ export const zModelWithDuplicateImports = z.object({ propC: zModelWithString.optional() }); -export const zModelThatExtends = z.unknown(); +export const zModelThatExtends = zModelWithString.merge(z.object({ + propExtendsA: z.string().optional(), + propExtendsB: zModelWithString.optional() +})); -export const zModelThatExtendsExtends = z.unknown(); +export const zModelThatExtendsExtends = zModelWithString.merge(zModelThatExtends).merge(z.object({ + propExtendsC: z.string().optional(), + propExtendsD: zModelWithString.optional() +})); export const zModelWithPattern = z.object({ key: z.string().max(64), @@ -354,7 +472,7 @@ export const zFile = z.object({ file: z.string().url().readonly().optional() }); -export const zdefault = z.object({ +export const zDefault = z.object({ name: z.string().optional() }); @@ -382,12 +500,33 @@ export const zModelWithAdditionalPropertiesEqTrue = z.object({ }); export const zNestedAnyOfArraysNullable = z.object({ - nullableArray: z.unknown().optional() + nullableArray: z.union([ + z.array(z.unknown()), + z.null() + ]).optional() }); -export const zCompositionWithOneOfAndProperties = z.unknown(); +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: z.unknown() + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.number().gte(0), + z.null() + ]), + qux: z.number().gte(0) +})); -export const zNullableObject = z.unknown(); +export const zNullableObject = z.union([ + z.object({ + foo: z.string().optional() + }), + z.null() +]); export const zCharactersInDescription = z.string(); @@ -395,7 +534,35 @@ export const zModelWithNullableObject = z.object({ data: zNullableObject.optional() }); -export const zModelWithOneOfEnum = z.unknown(); +export const zModelWithOneOfEnum = z.union([ + z.object({ + foo: z.enum([ + 'Bar' + ]) + }), + z.object({ + foo: z.enum([ + 'Baz' + ]) + }), + z.object({ + foo: z.enum([ + 'Qux' + ]) + }), + z.object({ + content: z.string().datetime(), + foo: z.enum([ + 'Quux' + ]) + }), + z.object({ + content: z.unknown(), + foo: z.enum([ + 'Corge' + ]) + }) +]); export const zModelWithNestedArrayEnumsDataFoo = z.enum([ 'foo', @@ -447,7 +614,16 @@ export const zModelWithBackticksInDescription = z.object({ template: z.string().optional() }); -export const zModelWithOneOfAndProperties = z.unknown(); +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + z.unknown(), + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.number().gte(0), + z.null() + ]), + qux: z.number().gte(0) +})); export const zParameterSimpleParameterUnused = z.string(); @@ -459,7 +635,7 @@ export const zDeleteFooData = z.string(); export const zDeleteFooData2 = z.string(); -export const zimport = z.string(); +export const zImport = z.string(); export const zSchemaWithFormRestrictedKeys = z.object({ description: z.string().optional(), @@ -483,14 +659,14 @@ export const zSchemaWithFormRestrictedKeys = z.object({ })).optional() }); -export const zio_k8s_apimachinery_pkg_apis_meta_v1_DeleteOptions = z.object({ +export const zIoK8sApimachineryPkgApisMetaV1DeleteOptions = z.object({ preconditions: z.object({ resourceVersion: z.string().optional(), uid: z.string().optional() }).optional() }); -export const zio_k8s_apimachinery_pkg_apis_meta_v1_Preconditions = z.object({ +export const zIoK8sApimachineryPkgApisMetaV1Preconditions = z.object({ resourceVersion: z.string().optional(), uid: z.string().optional() }); @@ -499,23 +675,125 @@ export const zAdditionalPropertiesUnknownIssue = z.object({}); export const zAdditionalPropertiesUnknownIssue2 = z.object({}); -export const zAdditionalPropertiesUnknownIssue3 = z.unknown(); +export const zAdditionalPropertiesUnknownIssue3 = z.intersection(z.string(), z.object({ + entries: z.object({}) +})); export const zAdditionalPropertiesIntegerIssue = z.object({ value: z.number() }); -export const zOneOfAllOfIssue = z.unknown(); +export const zOneOfAllOfIssue = z.union([ + z.intersection(z.union([ + zConstValue, + z.object({ + item: z.boolean().optional(), + error: z.union([ + z.string(), + z.null() + ]).optional(), + hasError: z.boolean().readonly().optional(), + data: z.object({}).optional() + }) + ]), z3eNum1Период), + z.object({ + item: z.union([ + z.string(), + z.null() + ]).optional(), + error: z.union([ + z.string(), + z.null() + ]).optional(), + hasError: z.boolean().readonly().optional() + }) +]); -export const zGeneric_Schema_Duplicate_Issue_1_System_Boolean_ = z.object({ +export const zGenericSchemaDuplicateIssue1SystemBoolean = z.object({ item: z.boolean().optional(), - error: z.unknown().optional(), + error: z.union([ + z.string(), + z.null() + ]).optional(), hasError: z.boolean().readonly().optional(), data: z.object({}).optional() }); -export const zGeneric_Schema_Duplicate_Issue_1_System_String_ = z.object({ - item: z.unknown().optional(), - error: z.unknown().optional(), +export const zGenericSchemaDuplicateIssue1SystemString = z.object({ + item: z.union([ + z.string(), + z.null() + ]).optional(), + error: z.union([ + z.string(), + z.null() + ]).optional(), hasError: z.boolean().readonly().optional() -}); \ No newline at end of file +}); + +export const zImportResponse = z.union([ + zModelFromZendesk, + zModelWithReadOnlyAndWriteOnly +]); + +export const zApiVVersionODataControllerCountResponse = zModelFromZendesk; + +export const zGetApiVbyApiVersionSimpleOperationResponse = z.number(); + +export const zPostCallWithOptionalParamResponse = z.union([ + z.number(), + z.void() +]); + +export const zCallWithNoContentResponseResponse = z.void(); + +export const zCallWithResponseAndNoContentResponseResponse = z.union([ + z.number(), + z.void() +]); + +export const zDummyAResponse = z400; + +export const zDummyBResponse = z.void(); + +export const zCallWithResponseResponse = zImport; + +export const zCallWithDuplicateResponsesResponse = z.union([ + zModelWithBoolean.merge(zModelWithInteger), + zModelWithString +]); + +export const zCallWithResponsesResponse = z.union([ + z.object({ + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().readonly().optional(), + value: z.array(zModelWithString).readonly().optional() + }), + zModelThatExtends, + zModelThatExtendsExtends +]); + +export const zTypesResponse = z.union([ + z.number(), + z.string(), + z.boolean(), + z.object({}) +]); + +export const zUploadFileResponse = z.boolean(); + +export const zFileResponseResponse = z.string(); + +export const zComplexTypesResponse = z.array(zModelWithString); + +export const zMultipartResponseResponse = z.object({ + file: z.string().optional(), + metadata: z.object({ + foo: z.string().optional(), + bar: z.string().optional() + }).optional() +}); + +export const zComplexParamsResponse = zModelWithString; + +export const zNonAsciiæøåÆøÅöôêÊ字符串Response = z.array(zNonAsciiStringæøåÆøÅöôêÊ字符串); \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/index.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/index.ts.snap index 55c4970c8..3a377c8ba 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/index.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/index.ts.snap @@ -64,8 +64,14 @@ export const createClient = (config: Config): Client => { let { data } = response; - if (opts.responseType === 'json' && opts.responseTransformer) { - data = await opts.responseTransformer(data); + if (opts.responseType === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } } return { diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/types.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/types.ts.snap index 56cacb235..927d791f5 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/types.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/client/types.ts.snap @@ -83,11 +83,16 @@ export interface Config */ querySerializer?: QuerySerializer | QuerySerializerOptions; /** - * A function for transforming response data before it's returned to the - * caller function. This is an ideal place to post-process server data, - * e.g. convert date ISO strings into native Date objects. + * 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; /** * Throw an error instead of returning it in the response? * @@ -97,7 +102,7 @@ export interface Config } export interface RequestOptions< - ThrowOnError extends boolean = false, + ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config { /** diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/index.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/index.ts.snap index 55c4970c8..3a377c8ba 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/index.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/index.ts.snap @@ -64,8 +64,14 @@ export const createClient = (config: Config): Client => { let { data } = response; - if (opts.responseType === 'json' && opts.responseTransformer) { - data = await opts.responseTransformer(data); + if (opts.responseType === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } } return { diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/types.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/types.ts.snap index 56cacb235..927d791f5 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/types.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle_transform/client/types.ts.snap @@ -83,11 +83,16 @@ export interface Config */ querySerializer?: QuerySerializer | QuerySerializerOptions; /** - * A function for transforming response data before it's returned to the - * caller function. This is an ideal place to post-process server data, - * e.g. convert date ISO strings into native Date objects. + * 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; /** * Throw an error instead of returning it in the response? * @@ -97,7 +102,7 @@ export interface Config } export interface RequestOptions< - ThrowOnError extends boolean = false, + ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config { /** diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/index.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/index.ts.snap index 2883b08cc..1985110ee 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/index.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/index.ts.snap @@ -106,8 +106,14 @@ export const createClient = (config: Config = {}): Client => { : opts.parseAs) ?? 'json'; let data = await response[parseAs](); - if (parseAs === 'json' && opts.responseTransformer) { - data = await opts.responseTransformer(data); + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } } return { diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/types.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/types.ts.snap index 0c914db6c..4049d690b 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/types.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/types.ts.snap @@ -88,11 +88,16 @@ export interface Config */ querySerializer?: QuerySerializer | QuerySerializerOptions; /** - * A function for transforming response data before it's returned to the - * caller function. This is an ideal place to post-process server data, - * e.g. convert date ISO strings into native Date objects. + * 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; /** * Throw an error instead of returning it in the response? * diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/index.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/index.ts.snap index 2883b08cc..1985110ee 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/index.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/index.ts.snap @@ -106,8 +106,14 @@ export const createClient = (config: Config = {}): Client => { : opts.parseAs) ?? 'json'; let data = await response[parseAs](); - if (parseAs === 'json' && opts.responseTransformer) { - data = await opts.responseTransformer(data); + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } } return { diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/types.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/types.ts.snap index 0c914db6c..4049d690b 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/types.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/types.ts.snap @@ -88,11 +88,16 @@ export interface Config */ querySerializer?: QuerySerializer | QuerySerializerOptions; /** - * A function for transforming response data before it's returned to the - * caller function. This is an ideal place to post-process server data, - * e.g. convert date ISO strings into native Date objects. + * 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; /** * Throw an error instead of returning it in the response? * diff --git a/packages/openapi-ts/test/index.test.ts b/packages/openapi-ts/test/index.test.ts index ee9378a44..f2ac8e145 100644 --- a/packages/openapi-ts/test/index.test.ts +++ b/packages/openapi-ts/test/index.test.ts @@ -492,7 +492,26 @@ describe('OpenAPI v3', () => { input: V3_TRANSFORMS_SPEC_PATH, output, plugins: [ - ...(config.plugins ?? []), + ...(config.plugins ?? []).map((plugin) => { + if (typeof plugin === 'string') { + if (plugin === '@hey-api/sdk') { + return { + // @ts-expect-error + ...plugin, + name: '@hey-api/sdk', + transformer: true, + }; + } + } else if (plugin.name === '@hey-api/sdk') { + return { + ...plugin, + name: '@hey-api/sdk', + transformer: true, + }; + } + + return plugin; + }), { dates: true, name: '@hey-api/transformers', diff --git a/packages/openapi-ts/test/plugins.test.ts b/packages/openapi-ts/test/plugins.test.ts index 10bae60fc..b04ecc073 100644 --- a/packages/openapi-ts/test/plugins.test.ts +++ b/packages/openapi-ts/test/plugins.test.ts @@ -212,11 +212,6 @@ for (const version of versions) { }, { config: createConfig({ - input: { - // TODO: parser - remove `exclude` once recursive references are handled - exclude: '^#/components/schemas/ModelWithCircularReference$', - path: path.join(__dirname, 'spec', version, 'full.json'), - }, output: 'default', plugins: ['zod'], }), diff --git a/packages/openapi-ts/test/sample.cjs b/packages/openapi-ts/test/sample.cjs index e2b9743ef..aef07ee26 100644 --- a/packages/openapi-ts/test/sample.cjs +++ b/packages/openapi-ts/test/sample.cjs @@ -5,15 +5,17 @@ const main = async () => { const config = { client: { // bundle: true, - name: '@hey-api/client-axios', + // name: '@hey-api/client-axios', // name: '@hey-api/client-fetch', + name: 'legacy/xhr', }, - experimentalParser: true, + // experimentalParser: true, input: { - exclude: '^#/components/schemas/ModelWithCircularReference$', + // exclude: '^#/components/schemas/ModelWithCircularReference$', // include: // '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$', - path: './test/spec/3.1.x/parameter-explode-false.json', + // path: './test/spec/3.1.x/full.json', + path: './test/spec/v3-transforms.json', // path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', // path: 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', }, @@ -34,12 +36,15 @@ const main = async () => { // type: 'json', }, { - // asClass: true, + asClass: true, // auth: false, // include... name: '@hey-api/sdk', // operationId: false, // serviceNameBuilder: '^Parameters', + // transformer: '@hey-api/transformers', + transformer: true, + // validator: 'zod', }, { dates: true, @@ -50,7 +55,7 @@ const main = async () => { // enums: 'typescript+namespace', enums: 'javascript', // exportInlineEnums: true, - identifierCase: 'preserve', + // identifierCase: 'preserve', name: '@hey-api/typescript', // tree: true, }, @@ -61,7 +66,7 @@ const main = async () => { // name: '@tanstack/vue-query', }, { - name: 'zod', + // name: 'zod', }, ], // useOptions: false,