From a79fac8919ed29eec7195cbd441ffa38b559d63c Mon Sep 17 00:00:00 2001 From: Lubos Date: Wed, 20 Nov 2024 23:40:44 +0800 Subject: [PATCH] fix: make Zod plugin available in plugins options --- .changeset/curly-ravens-kneel.md | 5 + .changeset/strange-rice-relate.md | 5 + .changeset/wet-laws-tell.md | 5 + docs/.vitepress/config/en.ts | 6 +- docs/embed.ts | 6 + docs/openapi-ts/configuration.md | 22 +- docs/openapi-ts/output.md | 4 + docs/openapi-ts/plugins.md | 164 ++++++ docs/openapi-ts/transformers.md | 2 +- docs/openapi-ts/zod.md | 43 ++ packages/openapi-ts/src/compiler/index.ts | 1 + packages/openapi-ts/src/compiler/types.ts | 16 +- .../src/openApi/3.0.x/parser/index.ts | 93 +++- .../src/openApi/3.1.x/parser/index.ts | 93 +++- .../src/openApi/shared/utils/filter.ts | 30 +- packages/openapi-ts/src/plugins/index.ts | 4 +- packages/openapi-ts/src/plugins/zod/plugin.ts | 334 ++++++++---- packages/openapi-ts/src/types/config.ts | 14 +- .../3.0.x/plugins/zod/default/zod.gen.ts | 508 ++++++++++++++---- .../3.1.x/plugins/zod/default/zod.gen.ts | 496 ++++++++++++++--- packages/openapi-ts/test/plugins.spec.ts | 11 +- packages/openapi-ts/test/sample.cjs | 1 + 22 files changed, 1527 insertions(+), 336 deletions(-) create mode 100644 .changeset/curly-ravens-kneel.md create mode 100644 .changeset/strange-rice-relate.md create mode 100644 .changeset/wet-laws-tell.md create mode 100644 docs/openapi-ts/plugins.md diff --git a/.changeset/curly-ravens-kneel.md b/.changeset/curly-ravens-kneel.md new file mode 100644 index 000000000..342d5a3bd --- /dev/null +++ b/.changeset/curly-ravens-kneel.md @@ -0,0 +1,5 @@ +--- +'@hey-api/docs': patch +--- + +docs: add Plugins page diff --git a/.changeset/strange-rice-relate.md b/.changeset/strange-rice-relate.md new file mode 100644 index 000000000..3117c044d --- /dev/null +++ b/.changeset/strange-rice-relate.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix: add input.exclude option diff --git a/.changeset/wet-laws-tell.md b/.changeset/wet-laws-tell.md new file mode 100644 index 000000000..9bfc69737 --- /dev/null +++ b/.changeset/wet-laws-tell.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix: make Zod plugin available in plugins options diff --git a/docs/.vitepress/config/en.ts b/docs/.vitepress/config/en.ts index fc02a9c79..94eccadc9 100644 --- a/docs/.vitepress/config/en.ts +++ b/docs/.vitepress/config/en.ts @@ -47,6 +47,10 @@ export default defineConfig({ }, { items: [ + { + link: '/openapi-ts/plugins', + text: 'Introduction', + }, { link: '/openapi-ts/fastify', text: 'Fastify', @@ -57,7 +61,7 @@ export default defineConfig({ }, { link: '/openapi-ts/zod', - text: 'Zod soon', + text: 'Zod', }, ], text: 'Plugins', diff --git a/docs/embed.ts b/docs/embed.ts index 02ac4cf4d..a0ed9352b 100644 --- a/docs/embed.ts +++ b/docs/embed.ts @@ -39,6 +39,12 @@ export const embedProject = (projectId: string) => async (event: Event) => { 'openapi-ts.config.ts,src/client/@tanstack/react-query.gen.ts,src/client/types.gen.ts,src/App.tsx', view: 'editor', }); + case 'hey-api-client-fetch-plugin-zod-example': + return await sdk.embedProjectId(container, projectId, { + height: 700, + openFile: 'openapi-ts.config.ts,src/client/zod.gen.ts,src/App.tsx', + view: 'editor', + }); case 'hey-api-example': return await sdk.embedProjectId(container, projectId, { height: 700, diff --git a/docs/openapi-ts/configuration.md b/docs/openapi-ts/configuration.md index f30645bda..ac8fc38dc 100644 --- a/docs/openapi-ts/configuration.md +++ b/docs/openapi-ts/configuration.md @@ -55,13 +55,16 @@ We use [`@apidevtools/json-schema-ref-parser`](https://github.com/APIDevTools/js Filters work only with the [experimental parser](#parser) which is currently an opt-in feature. ::: -If you work with large specifications and want to generate output from their subset, set `input.include` to a regular expression string matching against resource references. +If you work with large specifications and want to generate output from their subset, you can use regular expressions to select the relevant definitions. Set `input.include` to match resource references to be included or `input.exclude` to match resource references to be excluded. When both regular expressions match the same definition, `input.exclude` takes precedence over `input.include`. -```js +::: code-group + +```js [include] export default { client: '@hey-api/client-fetch', experimentalParser: true, // [!code ++] input: { + // match only the schema named `foo` and `GET` operation for the `/api/v1/foo` path // [!code ++] include: '^(#/components/schemas/foo|#/paths/api/v1/foo/get)$', // [!code ++] path: 'path/to/openapi.json', }, @@ -69,7 +72,20 @@ export default { }; ``` -The configuration above will process only the schema named `foo` and `GET` operation for the `/api/v1/foo` path. +```js [exclude] +export default { + client: '@hey-api/client-fetch', + experimentalParser: true, // [!code ++] + input: { + // match everything except for the schema named `foo` and `GET` operation for the `/api/v1/foo` path // [!code ++] + exclude: '^(#/components/schemas/foo|#/paths/api/v1/foo/get)$', // [!code ++] + path: 'path/to/openapi.json', + }, + output: 'src/client', +}; +``` + +::: ## Output diff --git a/docs/openapi-ts/output.md b/docs/openapi-ts/output.md index 62cffdbb6..c5dee244a 100644 --- a/docs/openapi-ts/output.md +++ b/docs/openapi-ts/output.md @@ -388,5 +388,9 @@ import type { Pet } from './client'; Client package files are located in the `client` folder. This folder will include different files depending on which client you're using. This folder isn't generated by default. If you want to bundle client packages into your output, read the [Bundling](/openapi-ts/clients/fetch#bundling) section. +## Plugins + +The default output generated by Hey API plugins already allows you to build robust clients. However, you might be working with third-party packages and wishing to automate more of your boilerplate. The [Plugins](/openapi-ts/plugins) page covers this topic and more. + diff --git a/docs/openapi-ts/plugins.md b/docs/openapi-ts/plugins.md new file mode 100644 index 000000000..14c09fa53 --- /dev/null +++ b/docs/openapi-ts/plugins.md @@ -0,0 +1,164 @@ +--- +title: Plugins +description: Learn about and discover available plugins. +--- + +# Plugins + +Every generated file in your output is created by a plugin. You already learned about the default plugins in [Output](/openapi-ts/output). This page contains all native plugins and shows you how to create your own. + +## Hey API + +Apart from being responsible for the default output, Hey API plugins are the foundation for other plugins. Instead of creating their own primitives, other plugins can reuse the artifacts from Hey API plugins. This results in smaller output and a better user experience. + +- `@hey-api/schemas` - export OpenAPI definitions as JavaScript objects +- `@hey-api/services` - robust and polished SDKs +- `@hey-api/transformers` - response data transformer functions +- `@hey-api/types` - TypeScript interfaces and enums + +## Third Party + +These plugins help reduce boilerplate associated with third-party dependencies. Hey API natively supports the most popular packages. Please open an issue on [GitHub](https://github.com/hey-api/openapi-ts/issues) if you'd like us to support your favorite package. + +- [`fastify`](/openapi-ts/fastify) - TypeScript interface for Fastify route handlers +- [`@tanstack/angular-query-experimental`](/openapi-ts/tanstack-query) - TanStack Query functions and query keys +- [`@tanstack/react-query`](/openapi-ts/tanstack-query) - TanStack Query functions and query keys +- [`@tanstack/solid-query`](/openapi-ts/tanstack-query) - TanStack Query functions and query keys +- [`@tanstack/svelte-query`](/openapi-ts/tanstack-query) - TanStack Query functions and query keys +- [`@tanstack/vue-query`](/openapi-ts/tanstack-query) - TanStack Query functions and query keys +- [`zod`](/openapi-ts/zod) - Zod schemas to validate your data + +## Community + +Featured community plugins. + +- [add plugin](https://github.com/hey-api/openapi-ts/pulls) + +## Custom + +::: warning +Plugins API is in development. The interface might change before it becomes stable. We encourage you to leave feedback on [GitHub](https://github.com/hey-api/openapi-ts/issues). +::: + +If the existing plugins do not handle your use case or you're working with proprietary packages, you might want to create your own plugin. + +### Configuration + +We recommend following the design pattern of the native plugins. First, create a `my-plugin` folder for your plugin files. Inside, create a barrel file `index.ts` exporting the plugin's API. + +::: code-group + +```ts [index.ts] +export { defaultConfig, defineConfig } from './config'; +export type { Config } from './types'; +``` + +::: + +`index.ts` references 2 files, so we need to create them. `types.d.ts` contains the TypeScript interface for your plugin's options. It must have the `name` and `output` fields, everything else will become your plugin's configuration options. + +::: code-group + +```ts [types.d.ts] +export interface Config { + /** + * Plugin name. Must be unique. + */ + name: 'my-plugin'; + /** + * Name of the generated file. + * @default 'my-plugin' + */ + output?: string; + /** + * A custom option for your plugin. + */ + myOption?: boolean; +} +``` + +::: + +`config.ts` contains the runtime configuration for your plugin. It must implement the `Config` interface from `types.d.ts` and additional plugin metadata defined in the `PluginConfig` interface. + +::: code-group + +```ts [config.ts] +import type { DefineConfig, PluginConfig } from '@hey-api/openapi-ts/plugins'; + +import { handler } from './plugin'; +import type { Config } from './types'; + +export const defaultConfig: PluginConfig = { + _dependencies: ['@hey-api/types'], + _handler: handler, + _handlerLegacy: () => {}, + name: 'my-plugin', + output: 'my-plugin', +}; + +/** + * Type helper for `my-plugin` plugin, returns {@link PluginConfig} object + */ +export const defineConfig: DefineConfig = (config) => ({ + ...defaultConfig, + ...config, +}); +``` + +::: + +In the `config.ts` above, we define a `my-plugin` plugin which will generate a `my-plugin.gen.ts` output file. We also demonstrate declaring `@hey-api/types` as a dependency for our plugin, so we can safely import artifacts from `types.gen.ts`. + +Lastly, we define the `_handler` method which will be responsible for generating our custom output. We just need to create the remaining `plugin.ts` file. + +::: code-group + +```ts [plugin.ts] +import type { PluginHandler } from '@hey-api/openapi-ts/plugins'; + +import type { Config } from './types'; + +export const handler: PluginHandler = ({ context, plugin }) => { + // create a file for our output + const file = context.createFile({ + id: plugin.name, + path: plugin.output, + }); + + context.subscribe('before', () => { + // do something before parsing the input + }); + + context.subscribe('operation', ({ operation }) => { + // do something with the operation model + }); + + context.subscribe('schema', ({ operation }) => { + // do something with the schema model + }); + + context.subscribe('after', () => { + // do something after parsing the input + }); +}; +``` + +::: + +And that's it! We can now register our plugin in the Hey API configuration. + +```js +import { defineConfig } from './src/my-plugin'; + +export default { + client: '@hey-api/client-fetch', + input: 'path/to/openapi.json', + output: 'src/client', + plugins: [ + defineConfig({ + myOption: true, + }), + ], +}; +``` diff --git a/docs/openapi-ts/transformers.md b/docs/openapi-ts/transformers.md index 7760a1412..5748c32e0 100644 --- a/docs/openapi-ts/transformers.md +++ b/docs/openapi-ts/transformers.md @@ -46,7 +46,7 @@ export default { }; ``` -This will generate types that use `Date` instead of `string` and appropriate transformers. Note that third party date packages are not supported at the moment. +This will generate types that use `Date` instead of `string` and appropriate transformers. Note that third-party date packages are not supported at the moment. ## Example diff --git a/docs/openapi-ts/zod.md b/docs/openapi-ts/zod.md index 78ec84c6c..e7ffd6609 100644 --- a/docs/openapi-ts/zod.md +++ b/docs/openapi-ts/zod.md @@ -10,3 +10,46 @@ Zod plugin is in development. You can follow the updates and provide feedback on ::: [Zod](https://zod.dev/) is a TypeScript-first schema validation library with static type inference. + + + +## Features + +- seamless integration with `@hey-api/openapi-ts` ecosystem +- Zod schemas for requests, responses, and reusable components + +## Installation + +::: warning +Zod plugin works only with the [experimental parser](/openapi-ts/configuration#parser) which is currently an opt-in feature. +::: + +Ensure you have already [configured](/openapi-ts/get-started) `@hey-api/openapi-ts`. Update your configuration to use the Zod plugin. + +```js +export default { + client: '@hey-api/client-fetch', + experimentalParser: true, // [!code ++] + input: 'path/to/openapi.json', + output: 'src/client', + plugins: [ + // ...other plugins + 'zod', // [!code ++] + ], +}; +``` + +You can now generate Zod artifacts. 🎉 + +## Output + +The Zod plugin will generate the following artifacts, depending on the input specification. + +## Schemas + +More information will be provided as we finalize the plugin. + + + diff --git a/packages/openapi-ts/src/compiler/index.ts b/packages/openapi-ts/src/compiler/index.ts index 57ec3becb..205595b8f 100644 --- a/packages/openapi-ts/src/compiler/index.ts +++ b/packages/openapi-ts/src/compiler/index.ts @@ -48,6 +48,7 @@ export const compiler = { parameterDeclaration: types.createParameterDeclaration, propertyAccessExpression: types.createPropertyAccessExpression, propertyAccessExpressions: transform.createPropertyAccessExpressions, + propertyAssignment: types.createPropertyAssignment, returnFunctionCall: _return.createReturnFunctionCall, returnStatement: _return.createReturnStatement, returnVariable: _return.createReturnVariable, diff --git a/packages/openapi-ts/src/compiler/types.ts b/packages/openapi-ts/src/compiler/types.ts index 0dc8d413c..f9cf7dad1 100644 --- a/packages/openapi-ts/src/compiler/types.ts +++ b/packages/openapi-ts/src/compiler/types.ts @@ -549,10 +549,10 @@ export const createObjectType = < ) { initializer = createIdentifier({ text: value.value as string }); } - assignment = ts.factory.createPropertyAssignment( - value.key, + assignment = createPropertyAssignment({ initializer, - ); + name: value.key, + }); } addLeadingComments({ @@ -598,7 +598,7 @@ export const createObjectType = < const assignment = shorthand && canShorthand ? ts.factory.createShorthandPropertyAssignment(value) - : ts.factory.createPropertyAssignment(key, initializer); + : createPropertyAssignment({ initializer, name: key }); return assignment; }) @@ -885,3 +885,11 @@ export const createBlock = ({ multiLine?: boolean; statements: Array; }) => ts.factory.createBlock(statements, multiLine); + +export const createPropertyAssignment = ({ + initializer, + name, +}: { + initializer: ts.Expression; + name: string | ts.PropertyName; +}) => ts.factory.createPropertyAssignment(name, initializer); diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/index.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/index.ts index 50fc0d95e..cf8fe7b9e 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/index.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/index.ts @@ -17,7 +17,10 @@ import { parseSchema } from './schema'; export const parseV3_0_X = (context: IRContext) => { const operationIds = new Map(); - const regexp = context.config.input.include + const excludeRegExp = context.config.input.exclude + ? new RegExp(context.config.input.exclude) + : undefined; + const includeRegExp = context.config.input.include ? new RegExp(context.config.input.include) : undefined; @@ -56,7 +59,14 @@ export const parseV3_0_X = (context: IRContext) => { }; const $refDelete = `#/paths${path}/delete`; - if (finalPathItem.delete && canProcessRef($refDelete, regexp)) { + if ( + finalPathItem.delete && + canProcessRef({ + $ref: $refDelete, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'delete', @@ -75,7 +85,14 @@ export const parseV3_0_X = (context: IRContext) => { } const $refGet = `#/paths${path}/get`; - if (finalPathItem.get && canProcessRef($refGet, regexp)) { + if ( + finalPathItem.get && + canProcessRef({ + $ref: $refGet, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'get', @@ -94,7 +111,14 @@ export const parseV3_0_X = (context: IRContext) => { } const $refHead = `#/paths${path}/head`; - if (finalPathItem.head && canProcessRef($refHead, regexp)) { + if ( + finalPathItem.head && + canProcessRef({ + $ref: $refHead, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'head', @@ -113,7 +137,14 @@ export const parseV3_0_X = (context: IRContext) => { } const $refOptions = `#/paths${path}/options`; - if (finalPathItem.options && canProcessRef($refOptions, regexp)) { + if ( + finalPathItem.options && + canProcessRef({ + $ref: $refOptions, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'options', @@ -132,7 +163,14 @@ export const parseV3_0_X = (context: IRContext) => { } const $refPatch = `#/paths${path}/patch`; - if (finalPathItem.patch && canProcessRef($refPatch, regexp)) { + if ( + finalPathItem.patch && + canProcessRef({ + $ref: $refPatch, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'patch', @@ -151,7 +189,14 @@ export const parseV3_0_X = (context: IRContext) => { } const $refPost = `#/paths${path}/post`; - if (finalPathItem.post && canProcessRef($refPost, regexp)) { + if ( + finalPathItem.post && + canProcessRef({ + $ref: $refPost, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'post', @@ -170,7 +215,14 @@ export const parseV3_0_X = (context: IRContext) => { } const $refPut = `#/paths${path}/put`; - if (finalPathItem.put && canProcessRef($refPut, regexp)) { + if ( + finalPathItem.put && + canProcessRef({ + $ref: $refPut, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'put', @@ -189,7 +241,14 @@ export const parseV3_0_X = (context: IRContext) => { } const $refTrace = `#/paths${path}/trace`; - if (finalPathItem.trace && canProcessRef($refTrace, regexp)) { + if ( + finalPathItem.trace && + canProcessRef({ + $ref: $refTrace, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'trace', @@ -212,7 +271,13 @@ export const parseV3_0_X = (context: IRContext) => { if (context.spec.components) { for (const name in context.spec.components.parameters) { const $ref = `#/components/parameters/${name}`; - if (!canProcessRef($ref, regexp)) { + if ( + !canProcessRef({ + $ref, + excludeRegExp, + includeRegExp, + }) + ) { continue; } @@ -231,7 +296,13 @@ export const parseV3_0_X = (context: IRContext) => { for (const name in context.spec.components.schemas) { const $ref = `#/components/schemas/${name}`; - if (!canProcessRef($ref, regexp)) { + if ( + !canProcessRef({ + $ref, + excludeRegExp, + includeRegExp, + }) + ) { continue; } diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/index.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/index.ts index 9cb9afb8a..1b4f40fda 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/index.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/index.ts @@ -17,7 +17,10 @@ import { parseSchema } from './schema'; export const parseV3_1_X = (context: IRContext) => { const operationIds = new Map(); - const regexp = context.config.input.include + const excludeRegExp = context.config.input.exclude + ? new RegExp(context.config.input.exclude) + : undefined; + const includeRegExp = context.config.input.include ? new RegExp(context.config.input.include) : undefined; @@ -49,7 +52,14 @@ export const parseV3_1_X = (context: IRContext) => { }; const $refDelete = `#/paths${path}/delete`; - if (finalPathItem.delete && canProcessRef($refDelete, regexp)) { + if ( + finalPathItem.delete && + canProcessRef({ + $ref: $refDelete, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'delete', @@ -68,7 +78,14 @@ export const parseV3_1_X = (context: IRContext) => { } const $refGet = `#/paths${path}/get`; - if (finalPathItem.get && canProcessRef($refGet, regexp)) { + if ( + finalPathItem.get && + canProcessRef({ + $ref: $refGet, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'get', @@ -87,7 +104,14 @@ export const parseV3_1_X = (context: IRContext) => { } const $refHead = `#/paths${path}/head`; - if (finalPathItem.head && canProcessRef($refHead, regexp)) { + if ( + finalPathItem.head && + canProcessRef({ + $ref: $refHead, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'head', @@ -106,7 +130,14 @@ export const parseV3_1_X = (context: IRContext) => { } const $refOptions = `#/paths${path}/options`; - if (finalPathItem.options && canProcessRef($refOptions, regexp)) { + if ( + finalPathItem.options && + canProcessRef({ + $ref: $refOptions, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'options', @@ -125,7 +156,14 @@ export const parseV3_1_X = (context: IRContext) => { } const $refPatch = `#/paths${path}/patch`; - if (finalPathItem.patch && canProcessRef($refPatch, regexp)) { + if ( + finalPathItem.patch && + canProcessRef({ + $ref: $refPatch, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'patch', @@ -144,7 +182,14 @@ export const parseV3_1_X = (context: IRContext) => { } const $refPost = `#/paths${path}/post`; - if (finalPathItem.post && canProcessRef($refPost, regexp)) { + if ( + finalPathItem.post && + canProcessRef({ + $ref: $refPost, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'post', @@ -163,7 +208,14 @@ export const parseV3_1_X = (context: IRContext) => { } const $refPut = `#/paths${path}/put`; - if (finalPathItem.put && canProcessRef($refPut, regexp)) { + if ( + finalPathItem.put && + canProcessRef({ + $ref: $refPut, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'put', @@ -182,7 +234,14 @@ export const parseV3_1_X = (context: IRContext) => { } const $refTrace = `#/paths${path}/trace`; - if (finalPathItem.trace && canProcessRef($refTrace, regexp)) { + if ( + finalPathItem.trace && + canProcessRef({ + $ref: $refTrace, + excludeRegExp, + includeRegExp, + }) + ) { parseOperation({ ...operationArgs, method: 'trace', @@ -205,7 +264,13 @@ export const parseV3_1_X = (context: IRContext) => { if (context.spec.components) { for (const name in context.spec.components.parameters) { const $ref = `#/components/parameters/${name}`; - if (!canProcessRef($ref, regexp)) { + if ( + !canProcessRef({ + $ref, + excludeRegExp, + includeRegExp, + }) + ) { continue; } @@ -224,7 +289,13 @@ export const parseV3_1_X = (context: IRContext) => { for (const name in context.spec.components.schemas) { const $ref = `#/components/schemas/${name}`; - if (!canProcessRef($ref, regexp)) { + if ( + !canProcessRef({ + $ref, + excludeRegExp, + includeRegExp, + }) + ) { continue; } diff --git a/packages/openapi-ts/src/openApi/shared/utils/filter.ts b/packages/openapi-ts/src/openApi/shared/utils/filter.ts index ba870f19a..02c2195e5 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/filter.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/filter.ts @@ -1,8 +1,30 @@ -export const canProcessRef = ($ref: string, regexp?: RegExp): boolean => { - if (!regexp) { +/** + * Exclude takes precedence over include. + */ +export const canProcessRef = ({ + $ref, + excludeRegExp, + includeRegExp, +}: { + $ref: string; + excludeRegExp?: RegExp; + includeRegExp?: RegExp; +}): boolean => { + if (!excludeRegExp && !includeRegExp) { return true; } - regexp.lastIndex = 0; - return regexp.test($ref); + if (excludeRegExp) { + excludeRegExp.lastIndex = 0; + if (excludeRegExp.test($ref)) { + return false; + } + } + + if (includeRegExp) { + includeRegExp.lastIndex = 0; + return includeRegExp.test($ref); + } + + return true; }; diff --git a/packages/openapi-ts/src/plugins/index.ts b/packages/openapi-ts/src/plugins/index.ts index fbcb68897..e454280a3 100644 --- a/packages/openapi-ts/src/plugins/index.ts +++ b/packages/openapi-ts/src/plugins/index.ts @@ -55,8 +55,8 @@ export type UserPlugins = | UserConfig | UserConfig | UserConfig - | UserConfig; -// | UserConfig + | UserConfig + | UserConfig; export type ClientPlugins = | PluginConfig diff --git a/packages/openapi-ts/src/plugins/zod/plugin.ts b/packages/openapi-ts/src/plugins/zod/plugin.ts index 654d8f1f7..a908dfd78 100644 --- a/packages/openapi-ts/src/plugins/zod/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/plugin.ts @@ -1,4 +1,4 @@ -import type ts from 'typescript'; +import ts from 'typescript'; import { compiler } from '../../compiler'; import type { IRContext } from '../../ir/context'; @@ -15,6 +15,12 @@ interface SchemaWithType['type']> const zodId = 'zod'; +const digitsRegExp = /^\d+$/; + +// frequently used identifiers +const optionalIdentifier = compiler.identifier({ text: 'optional' }); +const zIdentifier = compiler.identifier({ text: 'z' }); + const arrayTypeToZodSchema = ({ context, namespace, @@ -27,8 +33,8 @@ const arrayTypeToZodSchema = ({ if (!schema.items) { const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: 'array', + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), parameters: [ unknownTypeToZodSchema({ @@ -57,8 +63,8 @@ const arrayTypeToZodSchema = ({ if (itemExpressions.length === 1) { const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: 'array', + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), parameters: itemExpressions, }); @@ -77,8 +83,8 @@ const arrayTypeToZodSchema = ({ const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: 'array', + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), parameters: [ unknownTypeToZodSchema({ @@ -109,13 +115,61 @@ const booleanTypeToZodSchema = ({ const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: 'boolean', + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), }); return expression; }; +const enumTypeToZodSchema = ({ + context, + namespace, + schema, +}: { + context: IRContext; + namespace: Array; + schema: SchemaWithType<'enum'>; +}): ts.Expression => { + const enumMembers: Array = []; + + for (const item of schema.items ?? []) { + // Zod supports only string enums + if (item.type === 'string' && typeof item.const === 'string') { + enumMembers.push( + compiler.stringLiteral({ + text: item.const, + }), + ); + } + } + + if (!enumMembers.length) { + return unknownTypeToZodSchema({ + context, + namespace, + schema: { + type: 'unknown', + }, + }); + } + + const enumExpression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), + }), + parameters: [ + compiler.arrayLiteralExpression({ + elements: enumMembers, + multiLine: false, + }), + ], + }); + + return enumExpression; +}; + const neverTypeToZodSchema = ({ schema, }: { @@ -125,8 +179,8 @@ const neverTypeToZodSchema = ({ }) => { const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: schema.type, + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), }); return expression; @@ -141,8 +195,8 @@ const nullTypeToZodSchema = ({ }) => { const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: schema.type, + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), }); return expression; @@ -166,52 +220,82 @@ const numberTypeToZodSchema = ({ const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: 'number', + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), }); return expression; }; const objectTypeToZodSchema = ({ - // context, + context, // namespace, - // eslint-disable-next-line @typescript-eslint/no-unused-vars + schema, }: { context: IRContext; namespace: Array; schema: SchemaWithType<'object'>; }) => { + const properties: Array = []; + // let indexProperty: Property | undefined; // const schemaProperties: Array = []; // let indexPropertyItems: Array = []; - // const required = schema.required ?? []; + const required = schema.required ?? []; // let hasOptionalProperties = false; - // for (const name in schema.properties) { - // const property = schema.properties[name]; - // const isRequired = required.includes(name); - // digitsRegExp.lastIndex = 0; - // schemaProperties.push({ - // comment: parseSchemaJsDoc({ schema: property }), - // isReadOnly: property.accessScope === 'read', - // isRequired, - // name: digitsRegExp.test(name) - // ? ts.factory.createNumericLiteral(name) - // : name, - // type: schemaToZodSchema({ - // $ref: `${irRef}${name}`, - // context, - // namespace, - // schema: property, - // }), - // }); - // // indexPropertyItems.push(property); - // if (!isRequired) { - // hasOptionalProperties = true; - // } - // } + for (const name in schema.properties) { + const property = schema.properties[name]; + const isRequired = required.includes(name); + + let propertyExpression = schemaToZodSchema({ + context, + schema: property, + }); + + if (property.accessScope === 'read') { + propertyExpression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: propertyExpression, + name: compiler.identifier({ text: 'readonly' }), + }), + }); + } + + if (!isRequired) { + propertyExpression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: propertyExpression, + name: optionalIdentifier, + }), + }); + } + + digitsRegExp.lastIndex = 0; + let propertyName = digitsRegExp.test(name) + ? ts.factory.createNumericLiteral(name) + : name; + // TODO: parser - abstract safe property name logic + if ( + ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && + !name.startsWith("'") && + !name.endsWith("'") + ) { + propertyName = `'${name}'`; + } + properties.push( + compiler.propertyAssignment({ + initializer: propertyExpression, + name: propertyName, + }), + ); + + // indexPropertyItems.push(property); + // if (!isRequired) { + // hasOptionalProperties = true; + // } + } // if ( // schema.additionalProperties && @@ -253,16 +337,10 @@ const objectTypeToZodSchema = ({ // }); const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: 'object', + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), - parameters: [ - // TODO: parser - handle parameters - compiler.objectExpression({ - multiLine: true, - obj: [], - }), - ], + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], }); return expression; }; @@ -274,6 +352,13 @@ const stringTypeToZodSchema = ({ namespace: Array; schema: SchemaWithType<'string'>; }) => { + let stringExpression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), + }), + }); + if (schema.const !== undefined) { // TODO: parser - add constant // return compiler.literalTypeNode({ @@ -282,34 +367,47 @@ const stringTypeToZodSchema = ({ } if (schema.format) { - // TODO: parser - add format - // if (schema.format === 'binary') { - // return compiler.typeUnionNode({ - // types: [ - // compiler.typeReferenceNode({ - // typeName: 'Blob', - // }), - // compiler.typeReferenceNode({ - // typeName: 'File', - // }), - // ], - // }); - // } - // if (schema.format === 'date-time' || schema.format === 'date') { - // // TODO: parser - add ability to skip type transformers - // if (context.config.plugins['@hey-api/transformers']?.dates) { - // return compiler.typeReferenceNode({ typeName: 'Date' }); - // } - // } + switch (schema.format) { + case 'date-time': + stringExpression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: stringExpression, + name: compiler.identifier({ text: 'datetime' }), + }), + }); + break; + case 'ipv4': + case 'ipv6': + stringExpression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: stringExpression, + name: compiler.identifier({ text: 'ip' }), + }), + }); + break; + case 'uri': + stringExpression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: stringExpression, + name: compiler.identifier({ text: 'url' }), + }), + }); + break; + case 'date': + case 'email': + case 'time': + case 'uuid': + stringExpression = compiler.callExpression({ + functionName: compiler.propertyAccessExpression({ + expression: stringExpression, + name: compiler.identifier({ text: schema.format }), + }), + }); + break; + } } - const expression = compiler.callExpression({ - functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: 'string', - }), - }); - return expression; + return stringExpression; }; const undefinedTypeToZodSchema = ({ @@ -321,8 +419,8 @@ const undefinedTypeToZodSchema = ({ }) => { const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: schema.type, + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), }); return expression; @@ -337,8 +435,8 @@ const unknownTypeToZodSchema = ({ }) => { const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: schema.type, + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), }); return expression; @@ -353,8 +451,8 @@ const voidTypeToZodSchema = ({ }) => { const expression = compiler.callExpression({ functionName: compiler.propertyAccessExpression({ - expression: 'z', - name: schema.type, + expression: zIdentifier, + name: compiler.identifier({ text: schema.type }), }), }); return expression; @@ -370,7 +468,6 @@ const schemaTypeToZodSchema = ({ context: IRContext; namespace: Array; schema: IRSchemaObject; - // @ts-expect-error }): ts.Expression => { switch (schema.type as Required['type']) { case 'array': @@ -386,14 +483,11 @@ const schemaTypeToZodSchema = ({ schema: schema as SchemaWithType<'boolean'>, }); case 'enum': - // TODO: parser - handle enum - // return enumTypeToIdentifier({ - // $ref, - // context, - // namespace, - // schema: schema as SchemaWithType<'enum'>, - // }); - break; + return enumTypeToZodSchema({ + context, + namespace, + schema: schema as SchemaWithType<'enum'>, + }); case 'never': return neverTypeToZodSchema({ context, @@ -425,13 +519,20 @@ const schemaTypeToZodSchema = ({ schema: schema as SchemaWithType<'string'>, }); case 'tuple': - // TODO: parser - handle tuple - // return tupleTypeToIdentifier({ - // context, - // namespace, - // schema: schema as SchemaWithType<'tuple'>, - // }); - break; + // TODO: parser - temporary unknown while not handled + return unknownTypeToZodSchema({ + context, + namespace, + schema: { + type: 'unknown', + }, + }); + // TODO: parser - handle tuple + // return tupleTypeToIdentifier({ + // context, + // namespace, + // schema: schema as SchemaWithType<'tuple'>, + // }); case 'undefined': return undefinedTypeToZodSchema({ context, @@ -494,6 +595,15 @@ const schemaToZodSchema = ({ schema, }); } else if (schema.items) { + // TODO: parser - temporary unknown while not handled + expression = unknownTypeToZodSchema({ + context, + namespace, + schema: { + type: 'unknown', + }, + }); + // TODO: parser - handle items // schema = deduplicateSchema({ schema }); // if (schema.items) { @@ -527,24 +637,20 @@ const schemaToZodSchema = ({ } // emit nodes only if $ref points to a reusable component - if ($ref && isRefOpenApiComponent($ref) && expression) { - // enum handler emits its own artifacts - if (schema.type !== 'enum') { - const identifier = file.identifier({ - $ref, - create: true, - namespace: 'value', - }); - const statement = compiler.constVariable({ - exportConst: true, - expression, - name: identifier.name || '', - }); - file.add(statement); - } + if ($ref && isRefOpenApiComponent($ref)) { + const identifier = file.identifier({ + $ref, + create: true, + namespace: 'value', + }); + const statement = compiler.constVariable({ + exportConst: true, + expression, + name: identifier.name || '', + }); + file.add(statement); } - // @ts-expect-error return expression; }; diff --git a/packages/openapi-ts/src/types/config.ts b/packages/openapi-ts/src/types/config.ts index 3b9d9aa85..be02ad51b 100644 --- a/packages/openapi-ts/src/types/config.ts +++ b/packages/openapi-ts/src/types/config.ts @@ -81,9 +81,21 @@ export interface ClientConfig { | string | Record | { + /** + * Prevent parts matching the regular expression from being processed. + * You can select both operations and components by reference within + * the bundled input. In case of conflicts, `exclude` takes precedence + * over `include`. + * + * @example + * operation: '^#/paths/api/v1/foo/get$' + * schema: '^#/components/schemas/Foo$' + */ + exclude?: string; /** * Process only parts matching the regular expression. You can select both - * operations and components by reference within the bundled input. + * operations and components by reference within the bundled input. In + * case of conflicts, `exclude` takes precedence over `include`. * * @example * operation: '^#/paths/api/v1/foo/get$' 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 961e9f0d9..14c060977 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 @@ -30,31 +30,71 @@ export const NonAsciiStringæøåÆØÅöôêÊ字符串 = z.string(); export const SimpleFile = z.string(); -export const SimpleReference = z.object({}); +export const SimpleReference = z.object({ + prop: z.string().optional() +}); + +export const SimpleStringWithPattern = z.unknown(); + +export const EnumWithStrings = z.enum([ + 'Success', + 'Warning', + 'Error', + "'Single Quote'", + '"Double Quotes"', + 'Non-ascii: øæåôöØÆÅÔÖ字符串' +]); + +export const EnumWithReplacedCharacters = z.enum([ + "'Single Quote'", + '"Double Quotes"', + 'øæåôöØÆÅÔÖ字符串', + '' +]); + +export const EnumWithNumbers = z.unknown(); export const EnumFromDescription = z.number(); +export const EnumWithExtensions = z.unknown(); + +export const EnumWithXEnumNames = z.unknown(); + export const ArrayWithNumbers = z.array(z.number()); export const ArrayWithBooleans = z.array(z.boolean()); export const ArrayWithStrings = z.array(z.string()); -export const ArrayWithReferences = z.array(z.object({})); +export const ArrayWithReferences = z.array(z.object({ + prop: z.string().optional() +})); -export const ArrayWithArray = z.array(z.array(z.object({}))); +export const ArrayWithArray = z.array(z.array(z.object({ + prop: z.string().optional() +}))); -export const ArrayWithProperties = z.array(z.object({})); +export const ArrayWithProperties = z.array(z.object({ + '16x16': camelCaseCommentWithBreaks.optional(), + bar: z.string().optional() +})); export const ArrayWithAnyOfProperties = z.array(z.unknown()); -export const AnyOfAnyAndNull = z.object({}); +export const AnyOfAnyAndNull = z.object({ + data: z.unknown().optional() +}); -export const AnyOfArrays = z.object({}); +export const AnyOfArrays = z.object({ + results: z.array(z.unknown()).optional() +}); export const DictionaryWithString = z.object({}); -export const DictionaryWithPropertiesAndAdditionalProperties = z.object({}); +export const DictionaryWithPropertiesAndAdditionalProperties = z.object({ + foo: z.number().optional(), + bar: z.boolean().optional() +}); export const DictionaryWithReference = z.object({}); @@ -64,115 +104,356 @@ export const DictionaryWithDictionary = z.object({}); export const DictionaryWithProperties = z.object({}); -export const ModelWithInteger = z.object({}); +export const ModelWithInteger = z.object({ + prop: z.number().optional() +}); -export const ModelWithBoolean = z.object({}); +export const ModelWithBoolean = z.object({ + prop: z.boolean().optional() +}); -export const ModelWithString = z.object({}); +export const ModelWithString = z.object({ + prop: z.string().optional() +}); -export const ModelWithStringError = z.object({}); +export const ModelWithStringError = z.object({ + prop: z.string().optional() +}); export const Model_From_Zendesk = z.string(); -export const ModelWithNullableString = z.object({}); - -export const ModelWithEnum = z.object({}); - -export const ModelWithEnumWithHyphen = z.object({}); - -export const ModelWithEnumFromDescription = z.object({}); - -export const ModelWithNestedEnums = z.object({}); - -export const ModelWithReference = z.object({}); - -export const ModelWithArrayReadOnlyAndWriteOnly = z.object({}); - -export const ModelWithArray = z.object({}); - -export const ModelWithDictionary = z.object({}); - -export const DeprecatedModel = z.object({}); - -export const ModelWithCircularReference = z.object({}); - -export const CompositionWithOneOf = z.object({}); - -export const CompositionWithOneOfAnonymous = z.object({}); - -export const ModelCircle = z.object({}); - -export const ModelSquare = z.object({}); - -export const CompositionWithAnyOf = z.object({}); - -export const CompositionWithAnyOfAnonymous = z.object({}); - -export const CompositionWithNestedAnyAndTypeNull = z.object({}); - -export const CompositionWithNestedAnyOfAndNull = z.object({}); +export const ModelWithNullableString = z.object({ + nullableProp1: z.unknown().optional(), + nullableRequiredProp1: z.unknown(), + nullableProp2: z.unknown().optional(), + nullableRequiredProp2: z.unknown(), + 'foo_bar-enum': z.enum([ + 'Success', + 'Warning', + 'Error', + 'ØÆÅ字符串' + ]).optional() +}); + +export const ModelWithEnum = z.object({ + 'foo_bar-enum': z.enum([ + 'Success', + 'Warning', + 'Error', + 'ØÆÅ字符串' + ]).optional(), + statusCode: z.enum([ + '100', + '200 FOO', + '300 FOO_BAR', + '400 foo-bar', + '500 foo.bar', + '600 foo&bar' + ]).optional(), + bool: z.unknown().optional() +}); + +export const ModelWithEnumWithHyphen = z.object({ + 'foo-bar-baz-qux': z.enum([ + '3.0' + ]).optional() +}); + +export const ModelWithEnumFromDescription = z.object({ + test: z.number().optional() +}); + +export const ModelWithNestedEnums = z.object({ + dictionaryWithEnum: z.object({}).optional(), + dictionaryWithEnumFromDescription: z.object({}).optional(), + arrayWithEnum: z.array(z.enum([ + 'Success', + 'Warning', + 'Error' + ])).optional(), + arrayWithDescription: z.array(z.number()).optional(), + 'foo_bar-enum': z.enum([ + 'Success', + 'Warning', + 'Error', + 'ØÆÅ字符串' + ]).optional() +}); + +export const ModelWithReference = z.object({ + prop: z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.unknown(), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: ModelWithString.optional(), + 'property with space': z.string().optional(), + default: z.string().optional(), + try: z.string().optional(), + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().readonly().optional() + }).optional() +}); + +export const ModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.array(z.object({ + foo: z.string(), + bar: z.string().readonly(), + baz: z.string() + })).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + +export const ModelWithArray = z.object({ + prop: z.array(ModelWithString).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + +export const ModelWithDictionary = z.object({ + prop: z.object({}).optional() +}); + +export const DeprecatedModel = z.object({ + prop: z.string().optional() +}); + +export const CompositionWithOneOf = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithOneOfAnonymous = z.object({ + propA: z.unknown().optional() +}); + +export const ModelCircle = z.object({ + kind: z.string(), + radius: z.number().optional() +}); + +export const ModelSquare = z.object({ + kind: z.string(), + sideLength: z.number().optional() +}); + +export const CompositionWithOneOfDiscriminator = z.unknown(); + +export const CompositionWithAnyOf = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithAnyOfAnonymous = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithNestedAnyAndTypeNull = z.object({ + propA: z.unknown().optional() +}); + +export const _3e_num_1Период = z.enum([ + 'Bird', + 'Dog' +]); + +export const ConstValue = z.enum([ + 'ConstValue' +]); + +export const CompositionWithNestedAnyOfAndNull = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithOneOfAndNullable = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithOneOfAndSimpleDictionary = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithOneOfAndSimpleArrayDictionary = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithOneOfAndComplexArrayDictionary = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithAllOfAndNullable = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithAnyOfAndNullable = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionBaseModel = z.object({ + firstName: z.string().optional(), + lastname: z.string().optional() +}); + +export const CompositionExtendedModel = z.unknown(); + +export const ModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.unknown(), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: ModelWithString.optional(), + 'property with space': z.string().optional(), + default: z.string().optional(), + try: z.string().optional(), + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().readonly().optional() +}); + +export const ModelWithNestedProperties = z.object({ + first: z.unknown().readonly() +}); + +export const ModelWithDuplicateProperties = z.object({ + prop: ModelWithString.optional() +}); + +export const ModelWithOrderedProperties = z.object({ + zebra: z.string().optional(), + apple: z.string().optional(), + hawaii: z.string().optional() +}); + +export const ModelWithDuplicateImports = z.object({ + propA: ModelWithString.optional(), + propB: ModelWithString.optional(), + propC: ModelWithString.optional() +}); + +export const ModelThatExtends = z.unknown(); + +export const ModelThatExtendsExtends = z.unknown(); + +export const ModelWithPattern = z.object({ + key: z.string(), + name: z.string(), + enabled: z.boolean().readonly().optional(), + modified: z.string().datetime().readonly().optional(), + id: z.string().optional(), + text: z.string().optional(), + patternWithSingleQuotes: z.string().optional(), + patternWithNewline: z.string().optional(), + patternWithBacktick: z.string().optional() +}); + +export const File = z.object({ + id: z.string().readonly().optional(), + updated_at: z.string().datetime().readonly().optional(), + created_at: z.string().datetime().readonly().optional(), + mime: z.string(), + file: z.string().url().readonly().optional() +}); + +export const _default = z.object({ + name: z.string().optional() +}); + +export const Pageable = z.object({ + page: z.number().optional(), + size: z.number().optional(), + sort: z.array(z.string()).optional() +}); -export const CompositionWithOneOfAndNullable = z.object({}); - -export const CompositionWithOneOfAndSimpleDictionary = z.object({}); - -export const CompositionWithOneOfAndSimpleArrayDictionary = z.object({}); - -export const CompositionWithOneOfAndComplexArrayDictionary = z.object({}); +export const FreeFormObjectWithoutAdditionalProperties = z.object({}); -export const CompositionWithAllOfAndNullable = z.object({}); +export const FreeFormObjectWithAdditionalPropertiesEqTrue = z.object({}); -export const CompositionWithAnyOfAndNullable = z.object({}); +export const FreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.object({}); -export const CompositionBaseModel = z.object({}); +export const ModelWithConst = z.object({ + String: z.enum([ + 'String' + ]).optional(), + number: z.unknown().optional(), + null: z.unknown().optional(), + withType: z.enum([ + 'Some string' + ]).optional() +}); -export const ModelWithProperties = z.object({}); +export const ModelWithAdditionalPropertiesEqTrue = z.object({ + prop: z.string().optional() +}); -export const ModelWithNestedProperties = z.object({}); +export const NestedAnyOfArraysNullable = z.object({ + nullableArray: z.unknown().optional() +}); -export const ModelWithDuplicateProperties = z.object({}); +export const CompositionWithOneOfAndProperties = z.unknown(); -export const ModelWithOrderedProperties = z.object({}); +export const NullableObject = z.unknown(); -export const ModelWithDuplicateImports = z.object({}); +export const CharactersInDescription = z.string(); -export const ModelWithPattern = z.object({}); +export const ModelWithNullableObject = z.object({ + data: NullableObject.optional() +}); -export const File = z.object({}); +export const ModelWithOneOfEnum = z.unknown(); -export const _default = z.object({}); +export const ModelWithNestedArrayEnumsDataFoo = z.enum([ + 'foo', + 'bar' +]); -export const Pageable = z.object({}); +export const ModelWithNestedArrayEnumsDataBar = z.enum([ + 'baz', + 'qux' +]); -export const FreeFormObjectWithoutAdditionalProperties = z.object({}); +export const ModelWithNestedArrayEnumsData = z.object({ + foo: z.array(ModelWithNestedArrayEnumsDataFoo).optional(), + bar: z.array(ModelWithNestedArrayEnumsDataBar).optional() +}); -export const FreeFormObjectWithAdditionalPropertiesEqTrue = z.object({}); +export const ModelWithNestedArrayEnums = z.object({ + array_strings: z.array(z.string()).optional(), + data: ModelWithNestedArrayEnumsData.optional() +}); -export const FreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.object({}); +export const ModelWithNestedCompositionEnums = z.object({ + foo: ModelWithNestedArrayEnumsDataFoo.optional() +}); -export const ModelWithConst = z.object({}); +export const ModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.string().readonly(), + baz: z.string() +}); -export const ModelWithAdditionalPropertiesEqTrue = z.object({}); +export const ModelWithConstantSizeArray = z.unknown(); -export const NestedAnyOfArraysNullable = z.object({}); +export const ModelWithAnyOfConstantSizeArray = z.unknown(); -export const CharactersInDescription = z.string(); - -export const ModelWithNullableObject = z.object({}); - -export const ModelWithNestedArrayEnumsData = z.object({}); +export const ModelWithPrefixItemsConstantSizeArray = z.array(z.unknown()); -export const ModelWithNestedArrayEnums = z.object({}); +export const ModelWithAnyOfConstantSizeArrayNullable = z.unknown(); -export const ModelWithNestedCompositionEnums = z.object({}); +export const ModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.unknown(); -export const ModelWithReadOnlyAndWriteOnly = z.object({}); +export const ModelWithAnyOfConstantSizeArrayAndIntersect = z.unknown(); -export const ModelWithPrefixItemsConstantSizeArray = z.array(z.unknown()); +export const ModelWithNumericEnumUnion = z.object({ + value: z.unknown().optional() +}); -export const ModelWithNumericEnumUnion = z.object({}); +export const ModelWithBackticksInDescription = z.object({ + template: z.string().optional() +}); -export const ModelWithBackticksInDescription = z.object({}); +export const ModelWithOneOfAndProperties = z.unknown(); export const ParameterSimpleParameterUnused = z.string(); @@ -186,18 +467,61 @@ export const DeleteFooData2 = z.string(); export const _import = z.string(); -export const SchemaWithFormRestrictedKeys = z.object({}); - -export const io_k8s_apimachinery_pkg_apis_meta_v1_DeleteOptions = z.object({}); - -export const io_k8s_apimachinery_pkg_apis_meta_v1_Preconditions = z.object({}); +export const SchemaWithFormRestrictedKeys = z.object({ + description: z.string().optional(), + 'x-enum-descriptions': z.string().optional(), + 'x-enum-varnames': z.string().optional(), + 'x-enumNames': z.string().optional(), + title: z.string().optional(), + object: z.object({ + description: z.string().optional(), + 'x-enum-descriptions': z.string().optional(), + 'x-enum-varnames': z.string().optional(), + 'x-enumNames': z.string().optional(), + title: z.string().optional() + }).optional(), + array: z.array(z.object({ + description: z.string().optional(), + 'x-enum-descriptions': z.string().optional(), + 'x-enum-varnames': z.string().optional(), + 'x-enumNames': z.string().optional(), + title: z.string().optional() + })).optional() +}); + +export const io_k8s_apimachinery_pkg_apis_meta_v1_DeleteOptions = z.object({ + preconditions: z.object({ + resourceVersion: z.string().optional(), + uid: z.string().optional() + }).optional() +}); + +export const io_k8s_apimachinery_pkg_apis_meta_v1_Preconditions = z.object({ + resourceVersion: z.string().optional(), + uid: z.string().optional() +}); export const AdditionalPropertiesUnknownIssue = z.object({}); export const AdditionalPropertiesUnknownIssue2 = z.object({}); -export const AdditionalPropertiesIntegerIssue = z.object({}); +export const AdditionalPropertiesUnknownIssue3 = z.unknown(); + +export const AdditionalPropertiesIntegerIssue = z.object({ + value: z.number() +}); + +export const OneOfAllOfIssue = z.unknown(); -export const Generic_Schema_Duplicate_Issue_1_System_Boolean_ = z.object({}); +export const Generic_Schema_Duplicate_Issue_1_System_Boolean_ = z.object({ + item: z.boolean().optional(), + error: z.unknown().optional(), + hasError: z.boolean().readonly().optional(), + data: z.object({}).optional() +}); -export const Generic_Schema_Duplicate_Issue_1_System_String_ = z.object({}); \ No newline at end of file +export const Generic_Schema_Duplicate_Issue_1_System_String_ = z.object({ + item: z.unknown().optional(), + error: z.unknown().optional(), + hasError: z.boolean().readonly().optional() +}); \ 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 89be78661..8ce48d09d 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 @@ -30,31 +30,71 @@ export const NonAsciiStringæøåÆØÅöôêÊ字符串 = z.string(); export const SimpleFile = z.string(); -export const SimpleReference = z.object({}); +export const SimpleReference = z.object({ + prop: z.string().optional() +}); + +export const SimpleStringWithPattern = z.unknown(); + +export const EnumWithStrings = z.enum([ + 'Success', + 'Warning', + 'Error', + "'Single Quote'", + '"Double Quotes"', + 'Non-ascii: øæåôöØÆÅÔÖ字符串' +]); + +export const EnumWithReplacedCharacters = z.enum([ + "'Single Quote'", + '"Double Quotes"', + 'øæåôöØÆÅÔÖ字符串', + '' +]); + +export const EnumWithNumbers = z.unknown(); export const EnumFromDescription = z.number(); +export const EnumWithExtensions = z.unknown(); + +export const EnumWithXEnumNames = z.unknown(); + export const ArrayWithNumbers = z.array(z.number()); export const ArrayWithBooleans = z.array(z.boolean()); export const ArrayWithStrings = z.array(z.string()); -export const ArrayWithReferences = z.array(z.object({})); +export const ArrayWithReferences = z.array(z.object({ + prop: z.string().optional() +})); -export const ArrayWithArray = z.array(z.array(z.object({}))); +export const ArrayWithArray = z.array(z.array(z.object({ + prop: z.string().optional() +}))); -export const ArrayWithProperties = z.array(z.object({})); +export const ArrayWithProperties = z.array(z.object({ + '16x16': camelCaseCommentWithBreaks.optional(), + bar: z.string().optional() +})); export const ArrayWithAnyOfProperties = z.array(z.unknown()); -export const AnyOfAnyAndNull = z.object({}); +export const AnyOfAnyAndNull = z.object({ + data: z.unknown().optional() +}); -export const AnyOfArrays = z.object({}); +export const AnyOfArrays = z.object({ + results: z.array(z.unknown()).optional() +}); export const DictionaryWithString = z.object({}); -export const DictionaryWithPropertiesAndAdditionalProperties = z.object({}); +export const DictionaryWithPropertiesAndAdditionalProperties = z.object({ + foo: z.number().optional(), + bar: z.boolean().optional() +}); export const DictionaryWithReference = z.object({}); @@ -64,115 +104,350 @@ export const DictionaryWithDictionary = z.object({}); export const DictionaryWithProperties = z.object({}); -export const ModelWithInteger = z.object({}); +export const ModelWithInteger = z.object({ + prop: z.number().optional() +}); -export const ModelWithBoolean = z.object({}); +export const ModelWithBoolean = z.object({ + prop: z.boolean().optional() +}); -export const ModelWithString = z.object({}); +export const ModelWithString = z.object({ + prop: z.string().optional() +}); -export const ModelWithStringError = z.object({}); +export const ModelWithStringError = z.object({ + prop: z.string().optional() +}); export const Model_From_Zendesk = z.string(); -export const ModelWithNullableString = z.object({}); - -export const ModelWithEnum = z.object({}); - -export const ModelWithEnumWithHyphen = z.object({}); - -export const ModelWithEnumFromDescription = z.object({}); - -export const ModelWithNestedEnums = z.object({}); - -export const ModelWithReference = z.object({}); - -export const ModelWithArrayReadOnlyAndWriteOnly = z.object({}); - -export const ModelWithArray = z.object({}); - -export const ModelWithDictionary = z.object({}); - -export const DeprecatedModel = z.object({}); - -export const ModelWithCircularReference = z.object({}); - -export const CompositionWithOneOf = z.object({}); - -export const CompositionWithOneOfAnonymous = z.object({}); - -export const ModelCircle = z.object({}); - -export const ModelSquare = z.object({}); - -export const CompositionWithAnyOf = z.object({}); - -export const CompositionWithAnyOfAnonymous = z.object({}); - -export const CompositionWithNestedAnyAndTypeNull = z.object({}); +export const ModelWithNullableString = z.object({ + nullableProp1: z.unknown().optional(), + nullableRequiredProp1: z.unknown(), + nullableProp2: z.unknown().optional(), + nullableRequiredProp2: z.unknown(), + 'foo_bar-enum': z.enum([ + 'Success', + 'Warning', + 'Error', + 'ØÆÅ字符串' + ]).optional() +}); + +export const ModelWithEnum = z.object({ + 'foo_bar-enum': z.enum([ + 'Success', + 'Warning', + 'Error', + 'ØÆÅ字符串' + ]).optional(), + statusCode: z.enum([ + '100', + '200 FOO', + '300 FOO_BAR', + '400 foo-bar', + '500 foo.bar', + '600 foo&bar' + ]).optional(), + bool: z.unknown().optional() +}); + +export const ModelWithEnumWithHyphen = z.object({ + 'foo-bar-baz-qux': z.enum([ + '3.0' + ]).optional() +}); + +export const ModelWithEnumFromDescription = z.object({ + test: z.number().optional() +}); + +export const ModelWithNestedEnums = z.object({ + dictionaryWithEnum: z.object({}).optional(), + dictionaryWithEnumFromDescription: z.object({}).optional(), + arrayWithEnum: z.array(z.enum([ + 'Success', + 'Warning', + 'Error' + ])).optional(), + arrayWithDescription: z.array(z.number()).optional(), + 'foo_bar-enum': z.enum([ + 'Success', + 'Warning', + 'Error', + 'ØÆÅ字符串' + ]).optional() +}); + +export const ModelWithReference = z.object({ + prop: z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.unknown(), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: ModelWithString.optional(), + 'property with space': z.string().optional(), + default: z.string().optional(), + try: z.string().optional(), + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().readonly().optional() + }).optional() +}); + +export const ModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.array(z.object({ + foo: z.string(), + bar: z.string().readonly(), + baz: z.string() + })).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + +export const ModelWithArray = z.object({ + prop: z.array(ModelWithString).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + +export const ModelWithDictionary = z.object({ + prop: z.object({}).optional() +}); + +export const DeprecatedModel = z.object({ + prop: z.string().optional() +}); + +export const CompositionWithOneOf = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithOneOfAnonymous = z.object({ + propA: z.unknown().optional() +}); + +export const ModelCircle = z.object({ + kind: z.string(), + radius: z.number().optional() +}); + +export const ModelSquare = z.object({ + kind: z.string(), + sideLength: z.number().optional() +}); + +export const CompositionWithOneOfDiscriminator = z.unknown(); + +export const CompositionWithAnyOf = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithAnyOfAnonymous = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithNestedAnyAndTypeNull = z.object({ + propA: z.unknown().optional() +}); + +export const _3e_num_1Период = z.enum([ + 'Bird', + 'Dog' +]); export const ConstValue = z.string(); -export const CompositionWithNestedAnyOfAndNull = z.object({}); +export const CompositionWithNestedAnyOfAndNull = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithOneOfAndNullable = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithOneOfAndSimpleDictionary = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithOneOfAndSimpleArrayDictionary = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithOneOfAndComplexArrayDictionary = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithAllOfAndNullable = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionWithAnyOfAndNullable = z.object({ + propA: z.unknown().optional() +}); + +export const CompositionBaseModel = z.object({ + firstName: z.string().optional(), + lastname: z.string().optional() +}); + +export const CompositionExtendedModel = z.unknown(); + +export const ModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.unknown(), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: ModelWithString.optional(), + 'property with space': z.string().optional(), + default: z.string().optional(), + try: z.string().optional(), + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().readonly().optional() +}); + +export const ModelWithNestedProperties = z.object({ + first: z.unknown().readonly() +}); + +export const ModelWithDuplicateProperties = z.object({ + prop: ModelWithString.optional() +}); + +export const ModelWithOrderedProperties = z.object({ + zebra: z.string().optional(), + apple: z.string().optional(), + hawaii: z.string().optional() +}); + +export const ModelWithDuplicateImports = z.object({ + propA: ModelWithString.optional(), + propB: ModelWithString.optional(), + propC: ModelWithString.optional() +}); + +export const ModelThatExtends = z.unknown(); + +export const ModelThatExtendsExtends = z.unknown(); + +export const ModelWithPattern = z.object({ + key: z.string(), + name: z.string(), + enabled: z.boolean().readonly().optional(), + modified: z.string().datetime().readonly().optional(), + id: z.string().optional(), + text: z.string().optional(), + patternWithSingleQuotes: z.string().optional(), + patternWithNewline: z.string().optional(), + patternWithBacktick: z.string().optional() +}); + +export const File = z.object({ + id: z.string().readonly().optional(), + updated_at: z.string().datetime().readonly().optional(), + created_at: z.string().datetime().readonly().optional(), + mime: z.string(), + file: z.string().url().readonly().optional() +}); + +export const _default = z.object({ + name: z.string().optional() +}); + +export const Pageable = z.object({ + page: z.number().optional(), + size: z.number().optional(), + sort: z.array(z.string()).optional() +}); -export const CompositionWithOneOfAndNullable = z.object({}); +export const FreeFormObjectWithoutAdditionalProperties = z.object({}); -export const CompositionWithOneOfAndSimpleDictionary = z.object({}); +export const FreeFormObjectWithAdditionalPropertiesEqTrue = z.object({}); -export const CompositionWithOneOfAndSimpleArrayDictionary = z.object({}); +export const FreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.object({}); -export const CompositionWithOneOfAndComplexArrayDictionary = z.object({}); +export const ModelWithConst = z.object({ + String: z.string().optional(), + number: z.number().optional(), + null: z.null().optional(), + withType: z.string().optional() +}); -export const CompositionWithAllOfAndNullable = z.object({}); +export const ModelWithAdditionalPropertiesEqTrue = z.object({ + prop: z.string().optional() +}); -export const CompositionWithAnyOfAndNullable = z.object({}); +export const NestedAnyOfArraysNullable = z.object({ + nullableArray: z.unknown().optional() +}); -export const CompositionBaseModel = z.object({}); +export const CompositionWithOneOfAndProperties = z.unknown(); -export const ModelWithProperties = z.object({}); +export const NullableObject = z.unknown(); -export const ModelWithNestedProperties = z.object({}); +export const CharactersInDescription = z.string(); -export const ModelWithDuplicateProperties = z.object({}); +export const ModelWithNullableObject = z.object({ + data: NullableObject.optional() +}); -export const ModelWithOrderedProperties = z.object({}); +export const ModelWithOneOfEnum = z.unknown(); -export const ModelWithDuplicateImports = z.object({}); +export const ModelWithNestedArrayEnumsDataFoo = z.enum([ + 'foo', + 'bar' +]); -export const ModelWithPattern = z.object({}); +export const ModelWithNestedArrayEnumsDataBar = z.enum([ + 'baz', + 'qux' +]); -export const File = z.object({}); +export const ModelWithNestedArrayEnumsData = z.object({ + foo: z.array(ModelWithNestedArrayEnumsDataFoo).optional(), + bar: z.array(ModelWithNestedArrayEnumsDataBar).optional() +}); -export const _default = z.object({}); +export const ModelWithNestedArrayEnums = z.object({ + array_strings: z.array(z.string()).optional(), + data: ModelWithNestedArrayEnumsData.optional() +}); -export const Pageable = z.object({}); +export const ModelWithNestedCompositionEnums = z.object({ + foo: ModelWithNestedArrayEnumsDataFoo.optional() +}); -export const FreeFormObjectWithoutAdditionalProperties = z.object({}); +export const ModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.string().readonly(), + baz: z.string() +}); -export const FreeFormObjectWithAdditionalPropertiesEqTrue = z.object({}); +export const ModelWithConstantSizeArray = z.unknown(); -export const FreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.object({}); +export const ModelWithAnyOfConstantSizeArray = z.unknown(); -export const ModelWithConst = z.object({}); +export const ModelWithPrefixItemsConstantSizeArray = z.unknown(); -export const ModelWithAdditionalPropertiesEqTrue = z.object({}); +export const ModelWithAnyOfConstantSizeArrayNullable = z.unknown(); -export const NestedAnyOfArraysNullable = z.object({}); +export const ModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.unknown(); -export const CharactersInDescription = z.string(); +export const ModelWithAnyOfConstantSizeArrayAndIntersect = z.unknown(); -export const ModelWithNullableObject = z.object({}); +export const ModelWithNumericEnumUnion = z.object({ + value: z.unknown().optional() +}); -export const ModelWithNestedArrayEnumsData = z.object({}); +export const ModelWithBackticksInDescription = z.object({ + template: z.string().optional() +}); -export const ModelWithNestedArrayEnums = z.object({}); - -export const ModelWithNestedCompositionEnums = z.object({}); - -export const ModelWithReadOnlyAndWriteOnly = z.object({}); - -export const ModelWithNumericEnumUnion = z.object({}); - -export const ModelWithBackticksInDescription = z.object({}); +export const ModelWithOneOfAndProperties = z.unknown(); export const ParameterSimpleParameterUnused = z.string(); @@ -186,18 +461,61 @@ export const DeleteFooData2 = z.string(); export const _import = z.string(); -export const SchemaWithFormRestrictedKeys = z.object({}); - -export const io_k8s_apimachinery_pkg_apis_meta_v1_DeleteOptions = z.object({}); - -export const io_k8s_apimachinery_pkg_apis_meta_v1_Preconditions = z.object({}); +export const SchemaWithFormRestrictedKeys = z.object({ + description: z.string().optional(), + 'x-enum-descriptions': z.string().optional(), + 'x-enum-varnames': z.string().optional(), + 'x-enumNames': z.string().optional(), + title: z.string().optional(), + object: z.object({ + description: z.string().optional(), + 'x-enum-descriptions': z.string().optional(), + 'x-enum-varnames': z.string().optional(), + 'x-enumNames': z.string().optional(), + title: z.string().optional() + }).optional(), + array: z.array(z.object({ + description: z.string().optional(), + 'x-enum-descriptions': z.string().optional(), + 'x-enum-varnames': z.string().optional(), + 'x-enumNames': z.string().optional(), + title: z.string().optional() + })).optional() +}); + +export const io_k8s_apimachinery_pkg_apis_meta_v1_DeleteOptions = z.object({ + preconditions: z.object({ + resourceVersion: z.string().optional(), + uid: z.string().optional() + }).optional() +}); + +export const io_k8s_apimachinery_pkg_apis_meta_v1_Preconditions = z.object({ + resourceVersion: z.string().optional(), + uid: z.string().optional() +}); export const AdditionalPropertiesUnknownIssue = z.object({}); export const AdditionalPropertiesUnknownIssue2 = z.object({}); -export const AdditionalPropertiesIntegerIssue = z.object({}); +export const AdditionalPropertiesUnknownIssue3 = z.unknown(); + +export const AdditionalPropertiesIntegerIssue = z.object({ + value: z.number() +}); + +export const OneOfAllOfIssue = z.unknown(); -export const Generic_Schema_Duplicate_Issue_1_System_Boolean_ = z.object({}); +export const Generic_Schema_Duplicate_Issue_1_System_Boolean_ = z.object({ + item: z.boolean().optional(), + error: z.unknown().optional(), + hasError: z.boolean().readonly().optional(), + data: z.object({}).optional() +}); -export const Generic_Schema_Duplicate_Issue_1_System_String_ = z.object({}); \ No newline at end of file +export const Generic_Schema_Duplicate_Issue_1_System_String_ = z.object({ + item: z.unknown().optional(), + error: z.unknown().optional(), + hasError: z.boolean().readonly().optional() +}); \ No newline at end of file diff --git a/packages/openapi-ts/test/plugins.spec.ts b/packages/openapi-ts/test/plugins.spec.ts index 55632046a..d6785804c 100644 --- a/packages/openapi-ts/test/plugins.spec.ts +++ b/packages/openapi-ts/test/plugins.spec.ts @@ -22,12 +22,13 @@ for (const version of versions) { describe(`OpenAPI ${version} ${namespace}`, () => { const createConfig = ( userConfig: Omit & - Pick, 'plugins'>, + Pick, 'plugins'> & + Pick, 'input'>, ): UserConfig => ({ client: '@hey-api/client-fetch', experimentalParser: true, - ...userConfig, input: path.join(__dirname, 'spec', version, 'full.json'), + ...userConfig, output: path.join( outputDir, typeof userConfig.plugins[0] === 'string' @@ -211,8 +212,12 @@ 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', - // @ts-expect-error plugins: ['zod'], }), description: 'generate Zod schemas with Zod plugin', diff --git a/packages/openapi-ts/test/sample.cjs b/packages/openapi-ts/test/sample.cjs index 2cfabec64..e1c837712 100644 --- a/packages/openapi-ts/test/sample.cjs +++ b/packages/openapi-ts/test/sample.cjs @@ -11,6 +11,7 @@ const main = async () => { // debug: true, experimentalParser: true, input: { + exclude: '^#/components/schemas/ModelWithCircularReference$', // include: // '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$', path: './test/spec/3.1.x/full.json',