diff --git a/.changeset/cyan-regions-invite.md b/.changeset/cyan-regions-invite.md new file mode 100644 index 0000000000..29ba2b47bd --- /dev/null +++ b/.changeset/cyan-regions-invite.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix(validators): do not reference variables before they are declared diff --git a/.changeset/fast-views-attack.md b/.changeset/fast-views-attack.md new file mode 100644 index 0000000000..7650694933 --- /dev/null +++ b/.changeset/fast-views-attack.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix(renderer): allow duplicate names when one symbol is a type diff --git a/.changeset/wet-glasses-sleep.md b/.changeset/wet-glasses-sleep.md new file mode 100644 index 0000000000..7a8ad4ad52 --- /dev/null +++ b/.changeset/wet-glasses-sleep.md @@ -0,0 +1,5 @@ +--- +'@hey-api/codegen-core': patch +--- + +feat: add `isRegistered()` method to file and symbol registry diff --git a/.vscode/settings.json b/.vscode/settings.json index 6df8dbfe32..c4595f000a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,11 +3,12 @@ "source.fixAll.eslint": "explicit" }, "editor.quickSuggestions": { - "strings": true + "strings": "on" }, "eslint.format.enable": true, "eslint.nodePath": "./node_modules", "eslint.workingDirectories": [{ "pattern": "./packages/*/" }], "typescript.preferences.autoImportFileExcludePatterns": ["dist/**"], + "typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"], "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/packages/codegen-core/src/__tests__/files.test.ts b/packages/codegen-core/src/__tests__/files.test.ts index a90011a233..ef376d1a10 100644 --- a/packages/codegen-core/src/__tests__/files.test.ts +++ b/packages/codegen-core/src/__tests__/files.test.ts @@ -28,6 +28,10 @@ describe('FileRegistry', () => { expect(registry.get(file1.id)).toEqual(file1); expect(registry.get(['foo'])).toEqual(file1); + // isRegistered should be true for explicitly registered files + expect(registry.isRegistered(file1.id)).toBe(true); + expect(registry.isRegistered(['foo'])).toBe(true); + // Registering again with same selector returns same file const file1b = registry.register({ selector: ['foo'] }); expect(file1b).toEqual(file1); @@ -76,6 +80,17 @@ describe('FileRegistry', () => { expect(referenced).toContainEqual(file3); // Once registered, file1 is not in referenced set expect(referenced).not.toContainEqual(file1); + + // Referenced-only file should not be considered registered + expect(registry.isRegistered(file3.id)).toBe(false); + // Once registered, file3 becomes registered and no longer appears in referenced() + const file3Registered = registry.register({ + name: 'Baz', + selector: ['baz'], + }); + expect(registry.isRegistered(file3Registered.id)).toBe(true); + const referencedAfter = Array.from(registry.referenced()); + expect(referencedAfter).not.toContainEqual(file3Registered); }); it('throws on invalid register or reference', () => { diff --git a/packages/codegen-core/src/__tests__/symbols.test.ts b/packages/codegen-core/src/__tests__/symbols.test.ts index 4d905797f0..dfc85b2c67 100644 --- a/packages/codegen-core/src/__tests__/symbols.test.ts +++ b/packages/codegen-core/src/__tests__/symbols.test.ts @@ -27,6 +27,10 @@ describe('SymbolRegistry', () => { expect(registry.get(symbol1.id)).toEqual(symbol1); expect(registry.get(['foo'])).toEqual(symbol1); + // isRegistered should be true for explicitly registered symbols + expect(registry.isRegistered(symbol1.id)).toBe(true); + expect(registry.isRegistered(['foo'])).toBe(true); + // Registering again with same selector returns same symbol const symbol1b = registry.register({ selector: ['foo'] }); expect(symbol1b).toEqual(symbol1); @@ -75,6 +79,15 @@ describe('SymbolRegistry', () => { registry.setValue(symbol1.id, 42); expect(registry.hasValue(symbol1.id)).toBe(true); expect(registry.getValue(symbol1.id)).toBe(42); + + // referenced-only symbol should not be registered until register() with data + const symRef = registry.reference(['qux']); + expect(registry.isRegistered(symRef.id)).toBe(false); + const symRegistered = registry.register({ + placeholder: 'Qux', + selector: ['qux'], + }); + expect(registry.isRegistered(symRegistered.id)).toBe(true); }); it('throws on invalid register or reference', () => { diff --git a/packages/codegen-core/src/files/registry.ts b/packages/codegen-core/src/files/registry.ts index f0aca25210..e5c9f47d50 100644 --- a/packages/codegen-core/src/files/registry.ts +++ b/packages/codegen-core/src/files/registry.ts @@ -43,6 +43,11 @@ export class FileRegistry implements IFileRegistry { : { selector: symbolIdOrSelector }; } + isRegistered(fileIdOrSelector: number | ISelector): boolean { + const file = this.get(fileIdOrSelector); + return file ? this.registerOrder.has(file.id) : false; + } + reference(fileIdOrSelector: number | ISelector): IFileOut { const file = this.idOrSelector(fileIdOrSelector); return this.register(file); @@ -104,6 +109,9 @@ export class FileRegistry implements IFileRegistry { if (hasOtherKeys) { this.registerOrder.add(id); + if (this.referenceOrder.has(id)) { + this.referenceOrder.delete(id); + } } else { this.referenceOrder.add(id); } diff --git a/packages/codegen-core/src/files/types.d.ts b/packages/codegen-core/src/files/types.d.ts index 6ba2f7b80e..2c9f37ae2f 100644 --- a/packages/codegen-core/src/files/types.d.ts +++ b/packages/codegen-core/src/files/types.d.ts @@ -82,6 +82,13 @@ export interface IFileRegistry { * @returns File ID before being incremented */ readonly id: number; + /** + * Returns whether a file is registered in the registry. + * + * @param fileIdOrSelector File ID or selector to check. + * @returns True if the file is registered, false otherwise. + */ + isRegistered(fileIdOrSelector: number | ISelector): boolean; /** * Returns a file by ID or selector, registering it if it doesn't exist. * diff --git a/packages/codegen-core/src/symbols/registry.ts b/packages/codegen-core/src/symbols/registry.ts index a98987642b..22e78e61d1 100644 --- a/packages/codegen-core/src/symbols/registry.ts +++ b/packages/codegen-core/src/symbols/registry.ts @@ -51,6 +51,11 @@ export class SymbolRegistry implements ISymbolRegistry { : { selector: symbolIdOrSelector }; } + isRegistered(symbolIdOrSelector: number | ISelector): boolean { + const symbol = this.get(symbolIdOrSelector); + return symbol ? this.registerOrder.has(symbol.id) : false; + } + reference(symbolIdOrSelector: number | ISelector): ISymbolOut { const symbol = this.idOrSelector(symbolIdOrSelector); return this.register(symbol); diff --git a/packages/codegen-core/src/symbols/types.d.ts b/packages/codegen-core/src/symbols/types.d.ts index cd8f68111f..037575da88 100644 --- a/packages/codegen-core/src/symbols/types.d.ts +++ b/packages/codegen-core/src/symbols/types.d.ts @@ -113,6 +113,13 @@ export interface ISymbolRegistry { * @returns Symbol ID before being incremented. */ readonly id: number; + /** + * Returns whether a symbol is registered in the registry. + * + * @param symbolIdOrSelector Symbol ID or selector to check. + * @returns True if the symbol is registered, false otherwise. + */ + isRegistered(symbolIdOrSelector: number | ISelector): boolean; /** * Returns a symbol by ID or selector, registering it if it doesn't exist. * diff --git a/packages/config-vite-base/src/vitest.base.config.ts b/packages/config-vite-base/src/vitest.base.config.ts index 3d84f7be79..d9167e8349 100644 --- a/packages/config-vite-base/src/vitest.base.config.ts +++ b/packages/config-vite-base/src/vitest.base.config.ts @@ -1,4 +1,5 @@ -import { platform } from 'os'; +import { platform } from 'node:os'; + import type { ViteUserConfig } from 'vitest/config'; import { configDefaults, defineConfig, mergeConfig } from 'vitest/config'; diff --git a/packages/custom-client/src/plugin.ts b/packages/custom-client/src/plugin.ts index 7d4ddc8d7b..a696533098 100644 --- a/packages/custom-client/src/plugin.ts +++ b/packages/custom-client/src/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/package.json b/packages/openapi-ts-tests/main/package.json index df0d540b77..9a76ba1352 100644 --- a/packages/openapi-ts-tests/main/package.json +++ b/packages/openapi-ts-tests/main/package.json @@ -37,6 +37,7 @@ "@tanstack/svelte-query": "5.73.3", "@tanstack/vue-query": "5.73.3", "@types/cross-spawn": "6.0.6", + "arktype": "2.1.23", "axios": "1.8.2", "cross-spawn": "7.0.6", "eslint": "9.17.0", diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts index b7df7b1fef..c13ac1557a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts @@ -2,15 +2,6 @@ import * as v from 'valibot'; -export const vExternalSharedExternalSharedModel = v.object({ - id: v.string(), - name: v.optional(v.string()) -}); - -export const vExternalRefA = vExternalSharedExternalSharedModel; - -export const vExternalRefB = vExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const vNonAsciiStringæøåÆøÅöôêÊ字符串 = v.string(); */ export const vSimpleFile = v.string(); -/** - * This is a model with one string property - */ -export const vModelWithString = v.object({ - prop: v.optional(v.string()) -}); - -export const vSimpleReference = vModelWithString; - /** * This is a simple string */ @@ -130,16 +112,6 @@ export const vArrayWithBooleans = v.array(v.boolean()); */ export const vArrayWithStrings = v.array(v.string()); -/** - * This is a simple array with references - */ -export const vArrayWithReferences = v.array(vModelWithString); - -/** - * This is a simple array containing an array - */ -export const vArrayWithArray = v.array(v.array(vModelWithString)); - /** * This is a simple array with properties */ @@ -153,16 +125,6 @@ export const vArrayWithProperties = v.array(v.object({ */ export const vDictionaryWithString = v.object({}); -/** - * This is a string reference - */ -export const vDictionaryWithReference = v.object({}); - -/** - * This is a complex dictionary - */ -export const vDictionaryWithArray = v.object({}); - /** * This is a string dictionary */ @@ -195,6 +157,35 @@ export const vModelWithBoolean = v.object({ prop: v.optional(v.boolean()) }); +/** + * This is a model with one string property + */ +export const vModelWithString = v.object({ + prop: v.optional(v.string()) +}); + +export const vSimpleReference = vModelWithString; + +/** + * This is a simple array with references + */ +export const vArrayWithReferences = v.array(vModelWithString); + +/** + * This is a simple array containing an array + */ +export const vArrayWithArray = v.array(v.array(vModelWithString)); + +/** + * This is a string reference + */ +export const vDictionaryWithReference = v.object({}); + +/** + * This is a complex dictionary + */ +export const vDictionaryWithArray = v.object({}); + /** * This is a model with one string property */ @@ -258,30 +249,6 @@ export const vModelWithNestedEnums = v.object({ arrayWithDescription: v.optional(v.array(v.pipe(v.number(), v.integer()))) }); -/** - * This is a model with one nested property - */ -export const vModelWithProperties = v.object({ - required: v.string(), - requiredAndReadOnly: v.pipe(v.string(), v.readonly()), - string: v.optional(v.string()), - number: v.optional(v.number()), - boolean: v.optional(v.boolean()), - reference: v.optional(vModelWithString), - 'property with space': v.optional(v.string()), - default: v.optional(v.string()), - try: v.optional(v.string()), - '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), - '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) -}); - -/** - * This is a model with one property containing a reference - */ -export const vModelWithReference = v.object({ - prop: v.optional(vModelWithProperties) -}); - /** * This is a model with one property containing an array */ @@ -307,6 +274,30 @@ export const vModelWithCircularReference: v.GenericSchema = v.object({ })) }); +/** + * This is a model with one nested property + */ +export const vModelWithProperties = v.object({ + required: v.string(), + requiredAndReadOnly: v.pipe(v.string(), v.readonly()), + string: v.optional(v.string()), + number: v.optional(v.number()), + boolean: v.optional(v.boolean()), + reference: v.optional(vModelWithString), + 'property with space': v.optional(v.string()), + default: v.optional(v.string()), + try: v.optional(v.string()), + '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), + '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) +}); + +/** + * This is a model with one property containing a reference + */ +export const vModelWithReference = v.object({ + prop: v.optional(vModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -407,6 +398,15 @@ export const vFailureFailure = v.object({ reference_code: v.optional(v.string()) }); +export const vExternalSharedExternalSharedModel = v.object({ + id: v.string(), + name: v.optional(v.string()) +}); + +export const vExternalRefA = vExternalSharedExternalSharedModel; + +export const vExternalRefB = vExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts index ce60806c7e..af07c344a2 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts @@ -7,15 +7,6 @@ import * as v from 'valibot'; */ export const v400 = v.string(); -export const vExternalSharedExternalSharedModel = v.object({ - id: v.string(), - name: v.optional(v.string()) -}); - -export const vExternalRefA = vExternalSharedExternalSharedModel; - -export const vExternalRefB = vExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const vNonAsciiStringæøåÆøÅöôêÊ字符串 = v.string(); */ export const vSimpleFile = v.string(); -/** - * This is a model with one string property - */ -export const vModelWithString = v.object({ - prop: v.optional(v.string()) -}); - -/** - * This is a simple reference - */ -export const vSimpleReference = vModelWithString; - /** * This is a simple string */ @@ -158,16 +137,6 @@ export const vArrayWithBooleans = v.array(v.boolean()); */ export const vArrayWithStrings = v.optional(v.array(v.string()), ['test']); -/** - * This is a simple array with references - */ -export const vArrayWithReferences = v.array(vModelWithString); - -/** - * This is a simple array containing an array - */ -export const vArrayWithArray = v.array(v.array(vModelWithString)); - /** * This is a simple array with properties */ @@ -202,16 +171,6 @@ export const vDictionaryWithPropertiesAndAdditionalProperties = v.object({ bar: v.optional(v.boolean()) }); -/** - * This is a string reference - */ -export const vDictionaryWithReference = v.object({}); - -/** - * This is a complex dictionary - */ -export const vDictionaryWithArray = v.object({}); - /** * This is a string dictionary */ @@ -239,6 +198,38 @@ export const vModelWithBoolean = v.object({ prop: v.optional(v.boolean()) }); +/** + * This is a model with one string property + */ +export const vModelWithString = v.object({ + prop: v.optional(v.string()) +}); + +/** + * This is a simple reference + */ +export const vSimpleReference = vModelWithString; + +/** + * This is a simple array with references + */ +export const vArrayWithReferences = v.array(vModelWithString); + +/** + * This is a simple array containing an array + */ +export const vArrayWithArray = v.array(v.array(vModelWithString)); + +/** + * This is a string reference + */ +export const vDictionaryWithReference = v.object({}); + +/** + * This is a complex dictionary + */ +export const vDictionaryWithArray = v.object({}); + /** * This is a model with one string property */ @@ -336,48 +327,6 @@ export const vModelWithNestedEnums = v.object({ ])) }); -/** - * This is a model with one nested property - */ -export const vModelWithProperties = v.object({ - required: v.string(), - requiredAndReadOnly: v.pipe(v.string(), v.readonly()), - requiredAndNullable: v.union([ - v.string(), - v.null() - ]), - string: v.optional(v.string()), - number: v.optional(v.number()), - boolean: v.optional(v.boolean()), - reference: v.optional(vModelWithString), - 'property with space': v.optional(v.string()), - default: v.optional(v.string()), - try: v.optional(v.string()), - '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), - '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) -}); - -/** - * This is a model with one property containing a reference - */ -export const vModelWithReference = v.object({ - prop: v.optional(vModelWithProperties) -}); - -export const vModelWithReadOnlyAndWriteOnly = v.object({ - foo: v.string(), - bar: v.pipe(v.string(), v.readonly()) -}); - -/** - * This is a model with one property containing an array - */ -export const vModelWithArrayReadOnlyAndWriteOnly = v.object({ - prop: v.optional(v.array(vModelWithReadOnlyAndWriteOnly)), - propWithFile: v.optional(v.array(v.string())), - propWithNumber: v.optional(v.array(v.number())) -}); - /** * This is a model with one property containing an array */ @@ -628,6 +577,34 @@ export const vCompositionExtendedModel = v.intersect([ }) ]); +/** + * This is a model with one nested property + */ +export const vModelWithProperties = v.object({ + required: v.string(), + requiredAndReadOnly: v.pipe(v.string(), v.readonly()), + requiredAndNullable: v.union([ + v.string(), + v.null() + ]), + string: v.optional(v.string()), + number: v.optional(v.number()), + boolean: v.optional(v.boolean()), + reference: v.optional(vModelWithString), + 'property with space': v.optional(v.string()), + default: v.optional(v.string()), + try: v.optional(v.string()), + '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), + '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) +}); + +/** + * This is a model with one property containing a reference + */ +export const vModelWithReference = v.object({ + prop: v.optional(vModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -769,29 +746,6 @@ export const vNestedAnyOfArraysNullable = v.object({ ])) }); -/** - * This is a reusable parameter - */ -export const vSimpleParameter = v.unknown(); - -export const vCompositionWithOneOfAndProperties = v.intersect([ - v.union([ - v.object({ - foo: vSimpleParameter - }), - v.object({ - bar: vNonAsciiStringæøåÆøÅöôêÊ字符串 - }) - ]), - v.object({ - baz: v.union([ - v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), - v.null() - ]), - qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) - }) -]); - /** * An object that can be null */ @@ -868,6 +822,20 @@ export const vModelWithNestedCompositionEnums = v.object({ foo: v.optional(vModelWithNestedArrayEnumsDataFoo) }); +export const vModelWithReadOnlyAndWriteOnly = v.object({ + foo: v.string(), + bar: v.pipe(v.string(), v.readonly()) +}); + +/** + * This is a model with one property containing an array + */ +export const vModelWithArrayReadOnlyAndWriteOnly = v.object({ + prop: v.optional(v.array(vModelWithReadOnlyAndWriteOnly)), + propWithFile: v.optional(v.array(v.string())), + propWithNumber: v.optional(v.array(v.number())) +}); + export const vModelWithConstantSizeArray = v.tuple([ v.number(), v.number() @@ -908,22 +876,6 @@ export const vModelWithAnyOfConstantSizeArrayNullable = v.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const vImport = v.string(); - -export const vModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = v.tuple([ - v.union([ - v.number(), - vImport - ]), - v.union([ - v.number(), - vImport - ]) -]); - export const vModelWithAnyOfConstantSizeArrayAndIntersect = v.tuple([ v.intersect([ v.number(), @@ -946,20 +898,6 @@ export const vModelWithBackticksInDescription = v.object({ template: v.optional(v.string()) }); -export const vModelWithOneOfAndProperties = v.intersect([ - v.union([ - vSimpleParameter, - vNonAsciiStringæøåÆøÅöôêÊ字符串 - ]), - v.object({ - baz: v.union([ - v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), - v.null() - ]), - qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) - }) -]); - /** * Model used to test deduplication strategy (unused) */ @@ -985,6 +923,22 @@ export const vDeleteFooData = v.string(); */ export const vDeleteFooData2 = v.string(); +/** + * Model with restricted keyword name + */ +export const vImport = v.string(); + +export const vModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = v.tuple([ + v.union([ + v.number(), + vImport + ]), + v.union([ + v.number(), + vImport + ]) +]); + export const vSchemaWithFormRestrictedKeys = v.object({ description: v.optional(v.string()), 'x-enum-descriptions': v.optional(v.string()), @@ -1070,6 +1024,15 @@ export const vOneOfAllOfIssue = v.union([ vGenericSchemaDuplicateIssue1SystemString ]); +export const vExternalSharedExternalSharedModel = v.object({ + id: v.string(), + name: v.optional(v.string()) +}); + +export const vExternalRefA = vExternalSharedExternalSharedModel; + +export const vExternalRefB = vExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1132,6 +1095,43 @@ export const vGenericSchemaDuplicateIssue1SystemStringWritable = v.object({ ])) }); +/** + * This is a reusable parameter + */ +export const vSimpleParameter = v.string(); + +export const vCompositionWithOneOfAndProperties = v.intersect([ + v.union([ + v.object({ + foo: vSimpleParameter + }), + v.object({ + bar: vNonAsciiStringæøåÆøÅöôêÊ字符串 + }) + ]), + v.object({ + baz: v.union([ + v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), + v.null() + ]), + qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) + }) +]); + +export const vModelWithOneOfAndProperties = v.intersect([ + v.union([ + vSimpleParameter, + vNonAsciiStringæøåÆøÅöôêÊ字符串 + ]), + v.object({ + baz: v.union([ + v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), + v.null() + ]), + qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) + }) +]); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts index 9769db8fae..b327f2c83f 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts @@ -2,16 +2,14 @@ import * as v from 'valibot'; -export const vBar: v.GenericSchema = v.object({ - foo: v.optional(v.lazy(() => { - return vFoo; - })) -}); +export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); export const vFoo: v.GenericSchema = v.optional(v.union([ v.object({ foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: v.optional(vBar), + bar: v.optional(v.lazy(() => { + return vBar; + })), baz: v.optional(v.array(v.lazy(() => { return vFoo; }))), @@ -20,4 +18,6 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ v.null() ]), null); -export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); +export const vBar = v.object({ + foo: v.optional(vFoo) +}); diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-false/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-false/client/plugin.ts index 83305b41f7..c11a1606a6 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-false/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-false/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-number/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-number/client/plugin.ts index 83305b41f7..c11a1606a6 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-number/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-number/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-strict/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-strict/client/plugin.ts index 83305b41f7..c11a1606a6 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-strict/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-strict/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-string/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-string/client/plugin.ts index 83305b41f7..c11a1606a6 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-string/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-string/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/bundle/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/bundle/client/plugin.ts index 83305b41f7..c11a1606a6 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/bundle/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/bundle/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/default/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/default/client/plugin.ts index 83305b41f7..c11a1606a6 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/default/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/default/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-optional/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-optional/client/plugin.ts index 83305b41f7..c11a1606a6 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-optional/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-optional/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-required/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-required/client/plugin.ts index 83305b41f7..c11a1606a6 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-required/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-required/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts index fa8440fa99..c9ad4f08ae 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts @@ -7,21 +7,6 @@ import * as v from 'valibot'; */ export const v400 = v.string(); -export const vExternalSharedExternalSharedModel = v.object({ - id: v.string(), - name: v.optional(v.string()) -}); - -/** - * External ref to shared model (A) - */ -export const vExternalRefA = vExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const vExternalRefB = vExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const vNonAsciiStringæøåÆøÅöôêÊ字符串 = v.string(); */ export const vSimpleFile = v.string(); -/** - * This is a model with one string property - */ -export const vModelWithString = v.object({ - prop: v.optional(v.string()) -}); - -/** - * This is a simple reference - */ -export const vSimpleReference = vModelWithString; - /** * This is a simple string */ @@ -164,16 +137,6 @@ export const vArrayWithBooleans = v.array(v.boolean()); */ export const vArrayWithStrings = v.optional(v.array(v.string()), ['test']); -/** - * This is a simple array with references - */ -export const vArrayWithReferences = v.array(vModelWithString); - -/** - * This is a simple array containing an array - */ -export const vArrayWithArray = v.array(v.array(vModelWithString)); - /** * This is a simple array with properties */ @@ -211,16 +174,6 @@ export const vDictionaryWithPropertiesAndAdditionalProperties = v.object({ bar: v.optional(v.boolean()) }); -/** - * This is a string reference - */ -export const vDictionaryWithReference = v.object({}); - -/** - * This is a complex dictionary - */ -export const vDictionaryWithArray = v.object({}); - /** * This is a string dictionary */ @@ -248,6 +201,38 @@ export const vModelWithBoolean = v.object({ prop: v.optional(v.boolean()) }); +/** + * This is a model with one string property + */ +export const vModelWithString = v.object({ + prop: v.optional(v.string()) +}); + +/** + * This is a simple reference + */ +export const vSimpleReference = vModelWithString; + +/** + * This is a simple array with references + */ +export const vArrayWithReferences = v.array(vModelWithString); + +/** + * This is a simple array containing an array + */ +export const vArrayWithArray = v.array(v.array(vModelWithString)); + +/** + * This is a string reference + */ +export const vDictionaryWithReference = v.object({}); + +/** + * This is a complex dictionary + */ +export const vDictionaryWithArray = v.object({}); + /** * This is a model with one string property */ @@ -345,48 +330,6 @@ export const vModelWithNestedEnums = v.object({ ])) }); -/** - * This is a model with one nested property - */ -export const vModelWithProperties = v.object({ - required: v.string(), - requiredAndReadOnly: v.pipe(v.string(), v.readonly()), - requiredAndNullable: v.union([ - v.string(), - v.null() - ]), - string: v.optional(v.string()), - number: v.optional(v.number()), - boolean: v.optional(v.boolean()), - reference: v.optional(vModelWithString), - 'property with space': v.optional(v.string()), - default: v.optional(v.string()), - try: v.optional(v.string()), - '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), - '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) -}); - -/** - * This is a model with one property containing a reference - */ -export const vModelWithReference = v.object({ - prop: v.optional(vModelWithProperties) -}); - -export const vModelWithReadOnlyAndWriteOnly = v.object({ - foo: v.string(), - bar: v.pipe(v.string(), v.readonly()) -}); - -/** - * This is a model with one property containing an array - */ -export const vModelWithArrayReadOnlyAndWriteOnly = v.object({ - prop: v.optional(v.array(vModelWithReadOnlyAndWriteOnly)), - propWithFile: v.optional(v.array(v.string())), - propWithNumber: v.optional(v.array(v.number())) -}); - /** * This is a model with one property containing an array */ @@ -629,6 +572,34 @@ export const vCompositionExtendedModel = v.intersect([ }) ]); +/** + * This is a model with one nested property + */ +export const vModelWithProperties = v.object({ + required: v.string(), + requiredAndReadOnly: v.pipe(v.string(), v.readonly()), + requiredAndNullable: v.union([ + v.string(), + v.null() + ]), + string: v.optional(v.string()), + number: v.optional(v.number()), + boolean: v.optional(v.boolean()), + reference: v.optional(vModelWithString), + 'property with space': v.optional(v.string()), + default: v.optional(v.string()), + try: v.optional(v.string()), + '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), + '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) +}); + +/** + * This is a model with one property containing a reference + */ +export const vModelWithReference = v.object({ + prop: v.optional(vModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -766,29 +737,6 @@ export const vNestedAnyOfArraysNullable = v.object({ ])) }); -/** - * This is a reusable parameter - */ -export const vSimpleParameter = v.unknown(); - -export const vCompositionWithOneOfAndProperties = v.intersect([ - v.union([ - v.object({ - foo: vSimpleParameter - }), - v.object({ - bar: vNonAsciiStringæøåÆøÅöôêÊ字符串 - }) - ]), - v.object({ - baz: v.union([ - v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), - v.null() - ]), - qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) - }) -]); - /** * An object that can be null */ @@ -865,6 +813,20 @@ export const vModelWithNestedCompositionEnums = v.object({ foo: v.optional(vModelWithNestedArrayEnumsDataFoo) }); +export const vModelWithReadOnlyAndWriteOnly = v.object({ + foo: v.string(), + bar: v.pipe(v.string(), v.readonly()) +}); + +/** + * This is a model with one property containing an array + */ +export const vModelWithArrayReadOnlyAndWriteOnly = v.object({ + prop: v.optional(v.array(vModelWithReadOnlyAndWriteOnly)), + propWithFile: v.optional(v.array(v.string())), + propWithNumber: v.optional(v.array(v.number())) +}); + export const vModelWithConstantSizeArray = v.tuple([ v.number(), v.number() @@ -912,22 +874,6 @@ export const vModelWithAnyOfConstantSizeArrayNullable = v.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const vImport = v.string(); - -export const vModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = v.tuple([ - v.union([ - v.number(), - vImport - ]), - v.union([ - v.number(), - vImport - ]) -]); - export const vModelWithAnyOfConstantSizeArrayAndIntersect = v.tuple([ v.intersect([ v.number(), @@ -950,20 +896,6 @@ export const vModelWithBackticksInDescription = v.object({ template: v.optional(v.string()) }); -export const vModelWithOneOfAndProperties = v.intersect([ - v.union([ - vSimpleParameter, - vNonAsciiStringæøåÆøÅöôêÊ字符串 - ]), - v.object({ - baz: v.union([ - v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), - v.null() - ]), - qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) - }) -]); - /** * Model used to test deduplication strategy (unused) */ @@ -989,6 +921,22 @@ export const vDeleteFooData = v.string(); */ export const vDeleteFooData2 = v.string(); +/** + * Model with restricted keyword name + */ +export const vImport = v.string(); + +export const vModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = v.tuple([ + v.union([ + v.number(), + vImport + ]), + v.union([ + v.number(), + vImport + ]) +]); + export const vSchemaWithFormRestrictedKeys = v.object({ description: v.optional(v.string()), 'x-enum-descriptions': v.optional(v.string()), @@ -1074,6 +1022,21 @@ export const vOneOfAllOfIssue = v.union([ vGenericSchemaDuplicateIssue1SystemString ]); +export const vExternalSharedExternalSharedModel = v.object({ + id: v.string(), + name: v.optional(v.string()) +}); + +/** + * External ref to shared model (A) + */ +export const vExternalRefA = vExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const vExternalRefB = vExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1136,6 +1099,43 @@ export const vGenericSchemaDuplicateIssue1SystemStringWritable = v.object({ ])) }); +/** + * This is a reusable parameter + */ +export const vSimpleParameter = v.string(); + +export const vCompositionWithOneOfAndProperties = v.intersect([ + v.union([ + v.object({ + foo: vSimpleParameter + }), + v.object({ + bar: vNonAsciiStringæøåÆøÅöôêÊ字符串 + }) + ]), + v.object({ + baz: v.union([ + v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), + v.null() + ]), + qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) + }) +]); + +export const vModelWithOneOfAndProperties = v.intersect([ + v.union([ + vSimpleParameter, + vNonAsciiStringæøåÆøÅöôêÊ字符串 + ]), + v.object({ + baz: v.union([ + v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), + v.null() + ]), + qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) + }) +]); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts index 11af4d8fc1..769fabf361 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts @@ -11,6 +11,6 @@ export const vBar: v.GenericSchema = v.object({ ]) }); -export const vFoo: v.GenericSchema = v.object({ +export const vFoo = v.object({ foo: vBar }); diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref/valibot.gen.ts index c32b0b517f..f0dfcc0167 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref/valibot.gen.ts @@ -8,15 +8,15 @@ export const vBar: v.GenericSchema = v.object({ }))) }); -export const vFoo: v.GenericSchema = v.object({ +export const vFoo = v.object({ foo: v.optional(vBar) }); -/** - * description caused circular reference error - */ -export const vQux: v.GenericSchema = v.lazy(() => { +export const vBaz: v.GenericSchema = v.lazy(() => { return vQux; }); -export const vBaz: v.GenericSchema = vQux; +/** + * description caused circular reference error + */ +export const vQux = vBaz; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts index c54dd93e8a..0f25138dee 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts @@ -2,15 +2,10 @@ import * as v from 'valibot'; -/** - * This is Bar schema. - */ -export const vBar: v.GenericSchema = v.pipe(v.object({ - foo: v.optional(v.lazy(() => { - return vFoo; - })) -}), v.metadata({ - description: 'This is Bar schema.' +export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); + +export const vQux = v.record(v.string(), v.object({ + qux: v.optional(v.string()) })); /** @@ -21,7 +16,9 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ foo: v.optional(v.pipe(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/)), v.metadata({ description: 'This is foo property.' }))), - bar: v.optional(vBar), + bar: v.optional(v.lazy(() => { + return vBar; + })), baz: v.optional(v.pipe(v.array(v.lazy(() => { return vFoo; })), v.metadata({ @@ -34,10 +31,13 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ v.null() ]), null); -export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); - -export const vQux = v.record(v.string(), v.object({ - qux: v.optional(v.string()) +/** + * This is Bar schema. + */ +export const vBar = v.pipe(v.object({ + foo: v.optional(vFoo) +}), v.metadata({ + description: 'This is Bar schema.' })); /** diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts index 3c12cbfaa8..72e55fa049 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts @@ -2,14 +2,11 @@ import * as v from 'valibot'; -/** - * This is Bar schema. - */ -export const vBar: v.GenericSchema = v.object({ - foo: v.optional(v.lazy(() => { - return vFoo; - })) -}); +export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); + +export const vQux = v.record(v.string(), v.object({ + qux: v.optional(v.string()) +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const vBar: v.GenericSchema = v.object({ export const vFoo: v.GenericSchema = v.optional(v.union([ v.object({ foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: v.optional(vBar), + bar: v.optional(v.lazy(() => { + return vBar; + })), baz: v.optional(v.array(v.lazy(() => { return vFoo; }))), @@ -26,11 +25,12 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ v.null() ]), null); -export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); - -export const vQux = v.record(v.string(), v.object({ - qux: v.optional(v.string()) -})); +/** + * This is Bar schema. + */ +export const vBar = v.object({ + foo: v.optional(vFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts index 3c12cbfaa8..72e55fa049 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts @@ -2,14 +2,11 @@ import * as v from 'valibot'; -/** - * This is Bar schema. - */ -export const vBar: v.GenericSchema = v.object({ - foo: v.optional(v.lazy(() => { - return vFoo; - })) -}); +export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); + +export const vQux = v.record(v.string(), v.object({ + qux: v.optional(v.string()) +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const vBar: v.GenericSchema = v.object({ export const vFoo: v.GenericSchema = v.optional(v.union([ v.object({ foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: v.optional(vBar), + bar: v.optional(v.lazy(() => { + return vBar; + })), baz: v.optional(v.array(v.lazy(() => { return vFoo; }))), @@ -26,11 +25,12 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ v.null() ]), null); -export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); - -export const vQux = v.record(v.string(), v.object({ - qux: v.optional(v.string()) -})); +/** + * This is Bar schema. + */ +export const vBar = v.object({ + foo: v.optional(vFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/main/test/custom/client/plugin.ts b/packages/openapi-ts-tests/main/test/custom/client/plugin.ts index 83305b41f7..c11a1606a6 100644 --- a/packages/openapi-ts-tests/main/test/custom/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/custom/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts index 99a651ac59..68f1ac07c8 100644 --- a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts +++ b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts @@ -34,18 +34,20 @@ export default defineConfig(() => { // }, path: path.resolve( getSpecsPath(), - '3.0.x', + // '3.0.x', + '3.1.x', // 'circular.yaml', - 'dutchie.json', + // 'dutchie.json', // 'invalid', // 'openai.yaml', - // 'full.yaml', + 'full.yaml', // 'opencode.yaml', // 'sdk-instance.yaml', // 'string-with-format.yaml', // 'transformers.json', // 'type-format.yaml', // 'validators.yaml', + // 'validators-circular-ref.json', // 'validators-circular-ref-2.yaml', // 'zoom-video-sdk.json', ), @@ -213,7 +215,7 @@ export default defineConfig(() => { // }, }, { - asClass: true, + // asClass: true, // auth: false, // classNameBuilder: '{{name}}', // classNameBuilder: '{{name}}Service', @@ -234,10 +236,10 @@ export default defineConfig(() => { // signature: 'object', // transformer: '@hey-api/transformers', // transformer: true, - // validator: { - // request: 'valibot', - // response: 'zod', - // }, + validator: { + request: 'arktype', + response: 'arktype', + }, '~hooks': { symbols: { // getFilePath: (symbol) => { @@ -299,13 +301,19 @@ export default defineConfig(() => { }, }, }, + { + // name: 'arktype', + // types: { + // infer: true, + // }, + }, { // case: 'SCREAMING_SNAKE_CASE', // comments: false, // definitions: 'z{{name}}Definition', exportFromIndex: true, // metadata: true, - // name: 'valibot', + name: 'valibot', // requests: { // case: 'PascalCase', // name: '{{name}}Data', @@ -334,7 +342,7 @@ export default defineConfig(() => { { // case: 'snake_case', // comments: false, - compatibilityVersion: 'mini', + compatibilityVersion: 4, dates: { local: true, // offset: true, @@ -346,8 +354,8 @@ export default defineConfig(() => { // }, }, exportFromIndex: true, - metadata: true, - // name: 'zod', + // metadata: true, + name: 'zod', // requests: { // // case: 'SCREAMING_SNAKE_CASE', // // name: 'z{{name}}TestData', diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/default/zod.gen.ts index a914024a3d..ac1852b368 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/default/zod.gen.ts @@ -2,15 +2,6 @@ import * as z from 'zod/v4-mini'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string(), z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.optional(z.array(z.int())) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - /** * This is a model with one property containing an array */ @@ -326,13 +293,37 @@ export const zModelWithDictionary = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); } }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -426,6 +417,15 @@ export const zFailureFailure = z.object({ reference_code: z.optional(z.string()) }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/default/zod.gen.ts index fbea7e50be..2fe7ac3b5f 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/default/zod.gen.ts @@ -2,15 +2,6 @@ import { z } from 'zod'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.array(z.number().int()).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.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().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - /** * This is a model with one property containing an array */ @@ -331,6 +298,30 @@ export const zModelWithCircularReference: z.AnyZodObject = z.object({ }).optional() }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.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().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -424,6 +415,15 @@ export const zFailureFailure = z.object({ reference_code: z.string().optional() }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/default/zod.gen.ts index bfe8ab7354..c6888a77b9 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/default/zod.gen.ts @@ -2,15 +2,6 @@ import { z } from 'zod/v4'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string(), z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.optional(z.array(z.int())) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - /** * This is a model with one property containing an array */ @@ -326,13 +293,37 @@ export const zModelWithDictionary = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); } }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -426,6 +417,15 @@ export const zFailureFailure = z.object({ reference_code: z.optional(z.string()) }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts index 379f9a0449..9b3bf45710 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts @@ -2,15 +2,32 @@ import * as z from 'zod/v4-mini'; -export const zBaz = z.object({ - get quux(): z.ZodMiniOptional { - return z.optional(zQuux); +export const zFoo = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); -export const zCorge = z.object({ - get baz(): z.ZodMiniOptional { - return z.optional(z.array(zBaz)); +export const zBar = z.object({ + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { + return z.optional(z.lazy((): any => { + return zBaz; + })); + } +}); + +export const zBaz = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); @@ -18,32 +35,19 @@ export const zQux = z.union([ z.intersection(z.object({ type: z.literal('struct') }), z.lazy(() => { - return zCorge; + return z.lazy((): any => { + return zCorge; + }); })), z.intersection(z.object({ type: z.literal('array') - }), z.lazy(() => { - return zFoo; - })) + }), zFoo) ]); export const zQuux = z.object({ - get qux(): z.ZodMiniOptional { - return z.optional(zQux); - } -}); - -export const zFoo = z.object({ - quux: z.optional(zQuux) + qux: z.optional(zQux) }); -export const zBar = z.object({ - get bar(): z.ZodMiniOptional { - return z.optional(z.lazy((): any => { - return zBar; - })); - }, - get baz(): z.ZodMiniOptional { - return z.optional(zBaz); - } +export const zCorge = z.object({ + baz: z.optional(z.array(zBaz)) }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts index b2e3f459b3..8d5a0edfec 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts @@ -7,15 +7,6 @@ import * as z from 'zod/v4-mini'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z._default(z.array(z.string()), ['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.readonly(z.string()) -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -450,7 +399,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -665,6 +614,34 @@ export const zCompositionExtendedModel = z.intersection(zCompositionBaseModel, z lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -802,26 +779,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * An object that can be null */ @@ -898,6 +855,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: z.optional(zModelWithNestedArrayEnumsDataFoo) }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.readonly(z.string()) +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -942,22 +913,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -982,17 +937,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1018,6 +962,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1103,6 +1063,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1168,6 +1137,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/validators/zod.gen.ts index a8be5f51cd..9ef941af5b 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/validators/zod.gen.ts @@ -2,17 +2,17 @@ import * as z from 'zod/v4-mini'; -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -22,4 +22,6 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); +export const zBar = z.object({ + foo: z.optional(zFoo) +}); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts index abc26f9e65..bcce4d295c 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts @@ -2,38 +2,42 @@ import { z } from 'zod'; -export const zBaz: z.AnyZodObject = z.object({ +export const zFoo: z.AnyZodObject = z.object({ quux: z.lazy(() => { return zQuux; }).optional() }); -export const zCorge = z.object({ - baz: z.array(zBaz).optional() +export const zBar: z.AnyZodObject = z.object({ + bar: z.lazy(() => { + return zBar; + }).optional(), + baz: z.lazy(() => { + return zBaz; + }).optional() +}); + +export const zBaz: z.AnyZodObject = z.object({ + quux: z.lazy(() => { + return zQuux; + }).optional() }); export const zQux: z.ZodTypeAny = z.union([ z.object({ type: z.literal('struct') - }).and(zCorge), + }).and(z.lazy(() => { + return zCorge; + })), z.object({ type: z.literal('array') - }).and(z.lazy(() => { - return zFoo; - })) + }).and(zFoo) ]); export const zQuux = z.object({ qux: zQux.optional() }); -export const zFoo = z.object({ - quux: zQuux.optional() -}); - -export const zBar: z.AnyZodObject = z.object({ - bar: z.lazy(() => { - return zBar; - }).optional(), - baz: zBaz.optional() +export const zCorge = z.object({ + baz: z.array(zBaz).optional() }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts index a669c98676..2f0f272c35 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts @@ -7,15 +7,6 @@ import { z } from 'zod'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.boolean().optional() }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ]).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.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().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.string().readonly() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.array(zModelWithReadOnlyAndWriteOnly).optional(), - propWithFile: z.array(z.string()).optional(), - propWithNumber: z.array(z.number()).optional() -}); - /** * This is a model with one property containing an array */ @@ -663,6 +612,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.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().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -800,26 +777,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ]).optional() }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.unknown(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * An object that can be null */ @@ -896,6 +853,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: zModelWithNestedArrayEnumsDataFoo.optional() }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.string().readonly() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.array(zModelWithReadOnlyAndWriteOnly).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -940,22 +911,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -980,17 +935,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.string().optional() }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1016,6 +960,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.string().optional(), 'x-enum-descriptions': z.string().optional(), @@ -1101,6 +1061,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1166,6 +1135,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ]).optional() }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/validators/zod.gen.ts index a6537a8471..13daadc5e5 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/validators/zod.gen.ts @@ -2,16 +2,14 @@ import { z } from 'zod'; -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -20,4 +18,6 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); +export const zBar = z.object({ + foo: zFoo.optional() +}); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts index 24c4880fad..073c23da87 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts @@ -2,15 +2,32 @@ import { z } from 'zod/v4'; -export const zBaz = z.object({ - get quux(): z.ZodOptional { - return z.optional(zQuux); +export const zFoo = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); -export const zCorge = z.object({ - get baz(): z.ZodOptional { - return z.optional(z.array(zBaz)); +export const zBar = z.object({ + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { + return z.optional(z.lazy((): any => { + return zBaz; + })); + } +}); + +export const zBaz = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); @@ -18,32 +35,19 @@ export const zQux = z.union([ z.object({ type: z.literal('struct') }).and(z.lazy(() => { - return zCorge; + return z.lazy((): any => { + return zCorge; + }); })), z.object({ type: z.literal('array') - }).and(z.lazy(() => { - return zFoo; - })) + }).and(zFoo) ]); export const zQuux = z.object({ - get qux(): z.ZodOptional { - return z.optional(zQux); - } -}); - -export const zFoo = z.object({ - quux: z.optional(zQuux) + qux: z.optional(zQux) }); -export const zBar = z.object({ - get bar(): z.ZodOptional { - return z.optional(z.lazy((): any => { - return zBar; - })); - }, - get baz(): z.ZodOptional { - return z.optional(zBaz); - } +export const zCorge = z.object({ + baz: z.optional(z.array(zBaz)) }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts index 2ac7855c3e..eb441d3bb1 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts @@ -7,15 +7,6 @@ import { z } from 'zod/v4'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.string().readonly() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -450,7 +399,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -665,6 +614,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -802,26 +779,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * An object that can be null */ @@ -898,6 +855,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: z.optional(zModelWithNestedArrayEnumsDataFoo) }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.string().readonly() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -942,22 +913,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -982,17 +937,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1018,6 +962,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1103,6 +1063,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1168,6 +1137,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/validators/zod.gen.ts index d0fb4e7a81..5299554543 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/validators/zod.gen.ts @@ -2,17 +2,17 @@ import { z } from 'zod/v4'; -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -22,4 +22,6 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); +export const zBar = z.object({ + foo: z.optional(zFoo) +}); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts index 3875498e0e..395a1fa84e 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts @@ -7,21 +7,6 @@ import * as z from 'zod/v4-mini'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z._default(z.array(z.string()), ['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.readonly(z.string()) -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -459,7 +402,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -672,6 +615,34 @@ export const zCompositionExtendedModel = z.intersection(zCompositionBaseModel, z lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -805,26 +776,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * An object that can be null */ @@ -901,6 +852,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: z.optional(zModelWithNestedArrayEnumsDataFoo) }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.readonly(z.string()) +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -948,22 +913,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -988,17 +937,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1024,6 +962,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1109,6 +1063,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1174,6 +1143,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts index 5fa9cefc15..4c5643d938 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts @@ -3,7 +3,7 @@ import * as z from 'zod/v4-mini'; export const zBar = z.object({ - get bar(): z.ZodMiniOptional { + get bar() { return z.optional(z.array(z.lazy((): any => { return zBar; }))); @@ -14,11 +14,11 @@ export const zFoo = z.object({ foo: z.optional(zBar) }); -/** - * description caused circular reference error - */ -export const zQux = z.lazy((): any => { +export const zBaz = z.lazy((): any => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts index 3fd3ebc43e..b515009b63 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import * as z from 'zod/v4-mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts index 1a5edf41c9..64973d66ec 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts @@ -2,16 +2,11 @@ import * as z from 'zod/v4-mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}).register(z.globalRegistry, { - description: 'This is Bar schema.' -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -21,8 +16,12 @@ export const zFoo = z._default(z.union([ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/)).register(z.globalRegistry, { description: 'This is foo property.' })), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; })).register(z.globalRegistry, { @@ -36,11 +35,14 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}).register(z.globalRegistry, { + description: 'This is Bar schema.' +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts index a3170d6a4a..2b1c21593f 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import * as z from 'zod/v4-mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,8 +18,12 @@ export type BarZodType = z.infer; export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -32,15 +35,14 @@ export const zFoo = z._default(z.union([ export type FooZodType = z.infer; -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators/zod.gen.ts index 5bcc24c6f0..d3e255eb03 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators/zod.gen.ts @@ -2,14 +2,11 @@ import * as z from 'zod/v4-mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts index ee984a195b..b0e99007ac 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts @@ -7,21 +7,6 @@ import { z } from 'zod'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.boolean().optional() }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ]).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.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().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.string().readonly() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.array(zModelWithReadOnlyAndWriteOnly).optional(), - propWithFile: z.array(z.string()).optional(), - propWithNumber: z.array(z.number()).optional() -}); - /** * This is a model with one property containing an array */ @@ -670,6 +613,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.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().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -803,26 +774,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ]).optional() }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.unknown(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * An object that can be null */ @@ -899,6 +850,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: zModelWithNestedArrayEnumsDataFoo.optional() }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.string().readonly() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.array(zModelWithReadOnlyAndWriteOnly).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -946,22 +911,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -986,17 +935,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.string().optional() }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1022,6 +960,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.string().optional(), 'x-enum-descriptions': z.string().optional(), @@ -1107,6 +1061,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1172,6 +1141,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ]).optional() }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts index 528fe0d3d2..57536e8660 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts @@ -12,11 +12,11 @@ export const zFoo = z.object({ foo: zBar.optional() }); -/** - * description caused circular reference error - */ -export const zQux: z.ZodTypeAny = z.lazy(() => { +export const zBaz: z.ZodTypeAny = z.lazy(() => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts index 6eb670417d..620a0a8640 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts index 585fddbb26..4a09748c76 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}).describe('This is Bar schema.'); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).describe('This is foo property.').optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).describe('This is baz property.').optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}).describe('This is Bar schema.'); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts index ace814995d..d79d307e9b 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,7 +18,9 @@ export type BarZodType = z.infer; export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -30,15 +31,14 @@ export const zFoo: z.ZodTypeAny = z.union([ export type FooZodType = z.infer; -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators/zod.gen.ts index 83f84b2d0f..bcda6a22d8 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts index dcfa65b36a..ffd280f686 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts @@ -7,21 +7,6 @@ import { z } from 'zod/v4'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.string().readonly() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -459,7 +402,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -672,6 +615,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -805,26 +776,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * An object that can be null */ @@ -901,6 +852,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: z.optional(zModelWithNestedArrayEnumsDataFoo) }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.string().readonly() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -948,22 +913,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -988,17 +937,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1024,6 +962,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1109,6 +1063,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1174,6 +1143,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts index cc305b0c71..0fba2fc8b7 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts @@ -3,7 +3,7 @@ import { z } from 'zod/v4'; export const zBar = z.object({ - get bar(): z.ZodOptional { + get bar() { return z.optional(z.array(z.lazy((): any => { return zBar; }))); @@ -14,11 +14,11 @@ export const zFoo = z.object({ foo: z.optional(zBar) }); -/** - * description caused circular reference error - */ -export const zQux = z.lazy((): any => { +export const zBaz = z.lazy((): any => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts index fa1234e0a8..36e6f721aa 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod/v4'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts index 5e997ec271..270bc9a0d3 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts @@ -2,16 +2,11 @@ import { z } from 'zod/v4'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}).register(z.globalRegistry, { - description: 'This is Bar schema.' -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -21,8 +16,12 @@ export const zFoo = z.union([ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/).register(z.globalRegistry, { description: 'This is foo property.' })), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; })).register(z.globalRegistry, { @@ -36,11 +35,14 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}).register(z.globalRegistry, { + description: 'This is Bar schema.' +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts index 2794c6a1e9..c36f93b323 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import { z } from 'zod/v4'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,8 +18,12 @@ export type BarZodType = z.infer; export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -32,15 +35,14 @@ export const zFoo = z.union([ export type FooZodType = z.infer; -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators/zod.gen.ts index f9c56845ab..2b37813980 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod/v4'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/default/zod.gen.ts index b2bc94a140..e2f4e7158d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/default/zod.gen.ts @@ -2,15 +2,6 @@ import * as z from 'zod/mini'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string(), z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.optional(z.array(z.int())) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - /** * This is a model with one property containing an array */ @@ -326,13 +293,37 @@ export const zModelWithDictionary = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); } }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -426,6 +417,15 @@ export const zFailureFailure = z.object({ reference_code: z.optional(z.string()) }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/default/zod.gen.ts index 151cc9a841..aa5e6cbd5c 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/default/zod.gen.ts @@ -2,15 +2,6 @@ import { z } from 'zod/v3'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.array(z.number().int()).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.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().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - /** * This is a model with one property containing an array */ @@ -331,6 +298,30 @@ export const zModelWithCircularReference: z.AnyZodObject = z.object({ }).optional() }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.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().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -424,6 +415,15 @@ export const zFailureFailure = z.object({ reference_code: z.string().optional() }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/default/zod.gen.ts index 54967ed066..9e1e6dbadd 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/default/zod.gen.ts @@ -2,15 +2,6 @@ import { z } from 'zod'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string(), z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.optional(z.array(z.int())) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - /** * This is a model with one property containing an array */ @@ -326,13 +293,37 @@ export const zModelWithDictionary = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); } }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -426,6 +417,15 @@ export const zFailureFailure = z.object({ reference_code: z.optional(z.string()) }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts index c93fc275c6..0c5d13033f 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts @@ -2,15 +2,32 @@ import * as z from 'zod/mini'; -export const zBaz = z.object({ - get quux(): z.ZodMiniOptional { - return z.optional(zQuux); +export const zFoo = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); -export const zCorge = z.object({ - get baz(): z.ZodMiniOptional { - return z.optional(z.array(zBaz)); +export const zBar = z.object({ + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { + return z.optional(z.lazy((): any => { + return zBaz; + })); + } +}); + +export const zBaz = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); @@ -18,32 +35,19 @@ export const zQux = z.union([ z.intersection(z.object({ type: z.literal('struct') }), z.lazy(() => { - return zCorge; + return z.lazy((): any => { + return zCorge; + }); })), z.intersection(z.object({ type: z.literal('array') - }), z.lazy(() => { - return zFoo; - })) + }), zFoo) ]); export const zQuux = z.object({ - get qux(): z.ZodMiniOptional { - return z.optional(zQux); - } -}); - -export const zFoo = z.object({ - quux: z.optional(zQuux) + qux: z.optional(zQux) }); -export const zBar = z.object({ - get bar(): z.ZodMiniOptional { - return z.optional(z.lazy((): any => { - return zBar; - })); - }, - get baz(): z.ZodMiniOptional { - return z.optional(zBaz); - } +export const zCorge = z.object({ + baz: z.optional(z.array(zBaz)) }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts index 40e4f6ed44..c90cb4a7ce 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts @@ -7,15 +7,6 @@ import * as z from 'zod/mini'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z._default(z.array(z.string()), ['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.readonly(z.string()) -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -450,7 +399,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -665,6 +614,34 @@ export const zCompositionExtendedModel = z.intersection(zCompositionBaseModel, z lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -802,26 +779,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * An object that can be null */ @@ -898,6 +855,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: z.optional(zModelWithNestedArrayEnumsDataFoo) }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.readonly(z.string()) +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -942,22 +913,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -982,17 +937,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1018,6 +962,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1103,6 +1063,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1168,6 +1137,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/validators/zod.gen.ts index 77ab57f608..786ac3505a 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/validators/zod.gen.ts @@ -2,17 +2,17 @@ import * as z from 'zod/mini'; -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -22,4 +22,6 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); +export const zBar = z.object({ + foo: z.optional(zFoo) +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts index e73840e1d9..6011ab3e7c 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts @@ -2,38 +2,42 @@ import { z } from 'zod/v3'; -export const zBaz: z.AnyZodObject = z.object({ +export const zFoo: z.AnyZodObject = z.object({ quux: z.lazy(() => { return zQuux; }).optional() }); -export const zCorge = z.object({ - baz: z.array(zBaz).optional() +export const zBar: z.AnyZodObject = z.object({ + bar: z.lazy(() => { + return zBar; + }).optional(), + baz: z.lazy(() => { + return zBaz; + }).optional() +}); + +export const zBaz: z.AnyZodObject = z.object({ + quux: z.lazy(() => { + return zQuux; + }).optional() }); export const zQux: z.ZodTypeAny = z.union([ z.object({ type: z.literal('struct') - }).and(zCorge), + }).and(z.lazy(() => { + return zCorge; + })), z.object({ type: z.literal('array') - }).and(z.lazy(() => { - return zFoo; - })) + }).and(zFoo) ]); export const zQuux = z.object({ qux: zQux.optional() }); -export const zFoo = z.object({ - quux: zQuux.optional() -}); - -export const zBar: z.AnyZodObject = z.object({ - bar: z.lazy(() => { - return zBar; - }).optional(), - baz: zBaz.optional() +export const zCorge = z.object({ + baz: z.array(zBaz).optional() }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts index 2e56ac18cb..196b7b1dd8 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts @@ -7,15 +7,6 @@ import { z } from 'zod/v3'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.boolean().optional() }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ]).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.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().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.string().readonly() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.array(zModelWithReadOnlyAndWriteOnly).optional(), - propWithFile: z.array(z.string()).optional(), - propWithNumber: z.array(z.number()).optional() -}); - /** * This is a model with one property containing an array */ @@ -663,6 +612,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.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().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -800,26 +777,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ]).optional() }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.unknown(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * An object that can be null */ @@ -896,6 +853,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: zModelWithNestedArrayEnumsDataFoo.optional() }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.string().readonly() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.array(zModelWithReadOnlyAndWriteOnly).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -940,22 +911,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -980,17 +935,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.string().optional() }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1016,6 +960,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.string().optional(), 'x-enum-descriptions': z.string().optional(), @@ -1101,6 +1061,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1166,6 +1135,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ]).optional() }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/validators/zod.gen.ts index 1b13ab9cbe..884fe8b894 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/validators/zod.gen.ts @@ -2,16 +2,14 @@ import { z } from 'zod/v3'; -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -20,4 +18,6 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); +export const zBar = z.object({ + foo: zFoo.optional() +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts index 50b4fafee2..27ac2c0cc9 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts @@ -2,15 +2,32 @@ import { z } from 'zod'; -export const zBaz = z.object({ - get quux(): z.ZodOptional { - return z.optional(zQuux); +export const zFoo = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); -export const zCorge = z.object({ - get baz(): z.ZodOptional { - return z.optional(z.array(zBaz)); +export const zBar = z.object({ + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { + return z.optional(z.lazy((): any => { + return zBaz; + })); + } +}); + +export const zBaz = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); @@ -18,32 +35,19 @@ export const zQux = z.union([ z.object({ type: z.literal('struct') }).and(z.lazy(() => { - return zCorge; + return z.lazy((): any => { + return zCorge; + }); })), z.object({ type: z.literal('array') - }).and(z.lazy(() => { - return zFoo; - })) + }).and(zFoo) ]); export const zQuux = z.object({ - get qux(): z.ZodOptional { - return z.optional(zQux); - } -}); - -export const zFoo = z.object({ - quux: z.optional(zQuux) + qux: z.optional(zQux) }); -export const zBar = z.object({ - get bar(): z.ZodOptional { - return z.optional(z.lazy((): any => { - return zBar; - })); - }, - get baz(): z.ZodOptional { - return z.optional(zBaz); - } +export const zCorge = z.object({ + baz: z.optional(z.array(zBaz)) }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts index 0fb5789ee0..1eeb7d801e 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts @@ -7,15 +7,6 @@ import { z } from 'zod'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.string().readonly() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -450,7 +399,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -665,6 +614,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -802,26 +779,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * An object that can be null */ @@ -898,6 +855,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: z.optional(zModelWithNestedArrayEnumsDataFoo) }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.string().readonly() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -942,22 +913,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -982,17 +937,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1018,6 +962,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1103,6 +1063,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1168,6 +1137,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/validators/zod.gen.ts index 7f3ce000b5..f6b581f0d9 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/validators/zod.gen.ts @@ -2,17 +2,17 @@ import { z } from 'zod'; -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -22,4 +22,6 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); +export const zBar = z.object({ + foo: z.optional(zFoo) +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts index 3e732e02b8..e038574469 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts @@ -7,21 +7,6 @@ import * as z from 'zod/mini'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z._default(z.array(z.string()), ['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.readonly(z.string()) -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -459,7 +402,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -672,6 +615,34 @@ export const zCompositionExtendedModel = z.intersection(zCompositionBaseModel, z lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -805,26 +776,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * An object that can be null */ @@ -901,6 +852,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: z.optional(zModelWithNestedArrayEnumsDataFoo) }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.readonly(z.string()) +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -948,22 +913,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -988,17 +937,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1024,6 +962,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1109,6 +1063,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1174,6 +1143,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts index 2e65ce311d..33a0a739a4 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts @@ -3,7 +3,7 @@ import * as z from 'zod/mini'; export const zBar = z.object({ - get bar(): z.ZodMiniOptional { + get bar() { return z.optional(z.array(z.lazy((): any => { return zBar; }))); @@ -14,11 +14,11 @@ export const zFoo = z.object({ foo: z.optional(zBar) }); -/** - * description caused circular reference error - */ -export const zQux = z.lazy((): any => { +export const zBaz = z.lazy((): any => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts index f5db2ce5cb..d64c84a813 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import * as z from 'zod/mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts index 4b4759e2df..aaf540431c 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts @@ -2,16 +2,11 @@ import * as z from 'zod/mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}).register(z.globalRegistry, { - description: 'This is Bar schema.' -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -21,8 +16,12 @@ export const zFoo = z._default(z.union([ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/)).register(z.globalRegistry, { description: 'This is foo property.' })), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; })).register(z.globalRegistry, { @@ -36,11 +35,14 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}).register(z.globalRegistry, { + description: 'This is Bar schema.' +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts index 1eb0cfb954..19e14da91b 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import * as z from 'zod/mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,8 +18,12 @@ export type BarZodType = z.infer; export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -32,15 +35,14 @@ export const zFoo = z._default(z.union([ export type FooZodType = z.infer; -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators/zod.gen.ts index 0ee090a1ae..65b32c7ba9 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators/zod.gen.ts @@ -2,14 +2,11 @@ import * as z from 'zod/mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts index 87921e88a4..6dfbc46f47 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts @@ -7,21 +7,6 @@ import { z } from 'zod/v3'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.boolean().optional() }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ]).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.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().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.string().readonly() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.array(zModelWithReadOnlyAndWriteOnly).optional(), - propWithFile: z.array(z.string()).optional(), - propWithNumber: z.array(z.number()).optional() -}); - /** * This is a model with one property containing an array */ @@ -670,6 +613,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.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().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -803,26 +774,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ]).optional() }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.unknown(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * An object that can be null */ @@ -899,6 +850,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: zModelWithNestedArrayEnumsDataFoo.optional() }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.string().readonly() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.array(zModelWithReadOnlyAndWriteOnly).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -946,22 +911,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -986,17 +935,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.string().optional() }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1022,6 +960,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.string().optional(), 'x-enum-descriptions': z.string().optional(), @@ -1107,6 +1061,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1172,6 +1141,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ]).optional() }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts index 7e6cc3c892..466a9fc5b0 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts @@ -12,11 +12,11 @@ export const zFoo = z.object({ foo: zBar.optional() }); -/** - * description caused circular reference error - */ -export const zQux: z.ZodTypeAny = z.lazy(() => { +export const zBaz: z.ZodTypeAny = z.lazy(() => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts index 140dc5e8cd..746c1c5742 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod/v3'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts index efc9209a2f..396361b2aa 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod/v3'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}).describe('This is Bar schema.'); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).describe('This is foo property.').optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).describe('This is baz property.').optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}).describe('This is Bar schema.'); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts index 399d2c4138..7b92df5bc5 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import { z } from 'zod/v3'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,7 +18,9 @@ export type BarZodType = z.infer; export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -30,15 +31,14 @@ export const zFoo: z.ZodTypeAny = z.union([ export type FooZodType = z.infer; -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators/zod.gen.ts index fcc9c8c555..d4c6fe5369 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod/v3'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts index 6def19e85c..2602aa7436 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts @@ -7,21 +7,6 @@ import { z } from 'zod'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnly = z.object({ - foo: z.string(), - bar: z.string().readonly() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -459,7 +402,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -672,6 +615,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -805,26 +776,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * An object that can be null */ @@ -901,6 +852,20 @@ export const zModelWithNestedCompositionEnums = z.object({ foo: z.optional(zModelWithNestedArrayEnumsDataFoo) }); +export const zModelWithReadOnlyAndWriteOnly = z.object({ + foo: z.string(), + bar: z.string().readonly() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnly)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zModelWithConstantSizeArray = z.tuple([ z.number(), z.number() @@ -948,22 +913,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -988,17 +937,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1024,6 +962,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1109,6 +1063,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1174,6 +1143,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts index 1fa3416ddd..53ce2eb2f0 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; export const zBar = z.object({ - get bar(): z.ZodOptional { + get bar() { return z.optional(z.array(z.lazy((): any => { return zBar; }))); @@ -14,11 +14,11 @@ export const zFoo = z.object({ foo: z.optional(zBar) }); -/** - * description caused circular reference error - */ -export const zQux = z.lazy((): any => { +export const zBaz = z.lazy((): any => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts index 9b0132631a..bc6bd1d71e 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts index 34e6fe0968..4e71c97c64 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts @@ -2,16 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}).register(z.globalRegistry, { - description: 'This is Bar schema.' -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -21,8 +16,12 @@ export const zFoo = z.union([ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/).register(z.globalRegistry, { description: 'This is foo property.' })), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; })).register(z.globalRegistry, { @@ -36,11 +35,14 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}).register(z.globalRegistry, { + description: 'This is Bar schema.' +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts index 649cd05341..ba13cd685d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,8 +18,12 @@ export type BarZodType = z.infer; export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -32,15 +35,14 @@ export const zFoo = z.union([ export type FooZodType = z.infer; -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators/zod.gen.ts index 921e41c467..a48bf21863 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts/package.json b/packages/openapi-ts/package.json index ed7a15244f..fe0f11489e 100644 --- a/packages/openapi-ts/package.json +++ b/packages/openapi-ts/package.json @@ -91,7 +91,7 @@ }, "dependencies": { "@hey-api/codegen-core": "workspace:^0.3.0", - "@hey-api/json-schema-ref-parser": "1.2.0", + "@hey-api/json-schema-ref-parser": "1.2.1", "ansi-colors": "4.1.3", "c12": "3.3.1", "color-support": "1.1.3", diff --git a/packages/openapi-ts/src/createClient.ts b/packages/openapi-ts/src/createClient.ts index ecdf586d23..23f63ce236 100644 --- a/packages/openapi-ts/src/createClient.ts +++ b/packages/openapi-ts/src/createClient.ts @@ -8,6 +8,7 @@ import { generateOutput } from './generate/output'; import { getSpec } from './getSpec'; import type { IR } from './ir/types'; import { parseLegacy, parseOpenApiSpec } from './openApi'; +import { buildGraph } from './openApi/shared/utils/graph'; import { patchOpenApiSpec } from './openApi/shared/utils/patch'; import { processOutput } from './processOutput'; import type { Client } from './types/client'; @@ -332,8 +333,10 @@ export const createClient = async ({ context = parseOpenApiSpec({ config, dependencies, logger, spec: data }); } - // fallback to legacy parser - if (!context) { + if (context) { + context.graph = buildGraph(context.ir, logger).graph; + } else { + // fallback to legacy parser const parsed = parseLegacy({ openApi: data }); client = postProcessClient(parsed, config); } diff --git a/packages/openapi-ts/src/generate/__tests__/class.test.ts b/packages/openapi-ts/src/generate/__tests__/class.test.ts index 6060d05ed7..4d56bc0b23 100644 --- a/packages/openapi-ts/src/generate/__tests__/class.test.ts +++ b/packages/openapi-ts/src/generate/__tests__/class.test.ts @@ -85,7 +85,7 @@ describe('generateLegacyClientClass', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -97,7 +97,7 @@ describe('generateLegacyClientClass', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -108,8 +108,8 @@ describe('generateLegacyClientClass', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/generate/__tests__/core.test.ts b/packages/openapi-ts/src/generate/__tests__/core.test.ts index 6d81edfa6e..89b02629df 100644 --- a/packages/openapi-ts/src/generate/__tests__/core.test.ts +++ b/packages/openapi-ts/src/generate/__tests__/core.test.ts @@ -100,7 +100,7 @@ describe('generateLegacyCore', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -112,7 +112,7 @@ describe('generateLegacyCore', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -123,8 +123,8 @@ describe('generateLegacyCore', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', @@ -258,7 +258,7 @@ describe('generateLegacyCore', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -270,7 +270,7 @@ describe('generateLegacyCore', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -281,8 +281,8 @@ describe('generateLegacyCore', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', @@ -399,7 +399,7 @@ describe('generateLegacyCore', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -411,7 +411,7 @@ describe('generateLegacyCore', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -422,8 +422,8 @@ describe('generateLegacyCore', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/generate/__tests__/renderer.test.ts b/packages/openapi-ts/src/generate/__tests__/renderer.test.ts index 1c0cccb32c..95e6c373e4 100644 --- a/packages/openapi-ts/src/generate/__tests__/renderer.test.ts +++ b/packages/openapi-ts/src/generate/__tests__/renderer.test.ts @@ -2,6 +2,26 @@ import { describe, expect, it } from 'vitest'; import { TypeScriptRenderer } from '../renderer'; +// Minimal local BiMap for tests to avoid importing runtime-only class +class LocalBiMap { + private map = new Map(); + private reverse = new Map(); + get(key: Key) { + return this.map.get(key); + } + getKey(value: Value) { + return this.reverse.get(value); + } + set(key: Key, value: Value) { + this.map.set(key, value); + this.reverse.set(value, key); + return this; + } + hasValue(value: Value) { + return this.reverse.has(value); + } +} + describe('TypeScriptRenderer', () => { describe('default import placeholder replacement', () => { it('should replace placeholders in default imports correctly', () => { @@ -44,4 +64,59 @@ describe('TypeScriptRenderer', () => { expect(importLines).toEqual(["import foo from 'foo';"]); }); }); + + describe('replacer duplicate name handling', () => { + it('allows duplicate names when kinds differ (type vs value)', () => { + const renderer = new TypeScriptRenderer(); + + // Prepare a mock file and project + const file: any = { + resolvedNames: new LocalBiMap(), + symbols: { body: [], exports: [], imports: [] }, + }; + + const project = { + symbolIdToFiles: () => [file], + symbols: new Map(), + } as any; + + // Two symbols with the same name but different kinds + const typeSymbolId = 1; + const valueSymbolId = 2; + + const typeSymbol: any = { + exportFrom: [], + id: typeSymbolId, + meta: { kind: 'type' }, + name: 'Foo', + placeholder: '_heyapi_1_', + }; + const valueSymbol: any = { + exportFrom: [], + id: valueSymbolId, + meta: {}, + name: 'Foo', + placeholder: '_heyapi_2_', + }; + + project.symbols.set(typeSymbolId, typeSymbol); + project.symbols.set(valueSymbolId, valueSymbol); + + // First replacement should register the name 'Foo' + const first = renderer['replacerFn']({ + file, + project, + symbol: typeSymbol, + }); + expect(first).toEqual('Foo'); + + // Second replacement (different kind) should be allowed to also use 'Foo' + const second = renderer['replacerFn']({ + file, + project, + symbol: valueSymbol, + }); + expect(second).toEqual('Foo'); + }); + }); }); diff --git a/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts b/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts index f7350e4388..03c272773c 100644 --- a/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts +++ b/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts @@ -84,7 +84,7 @@ describe('generateIndexFile', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -96,7 +96,7 @@ describe('generateIndexFile', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -107,8 +107,8 @@ describe('generateIndexFile', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts b/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts index adc0321640..6148cb2ebb 100644 --- a/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts +++ b/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts @@ -97,7 +97,7 @@ describe('generateLegacyOutput', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -109,7 +109,7 @@ describe('generateLegacyOutput', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -120,8 +120,8 @@ describe('generateLegacyOutput', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/generate/renderer.ts b/packages/openapi-ts/src/generate/renderer.ts index f6e107b8f9..9a25299f24 100644 --- a/packages/openapi-ts/src/generate/renderer.ts +++ b/packages/openapi-ts/src/generate/renderer.ts @@ -479,8 +479,16 @@ export class TypeScriptRenderer implements Renderer { const [symbolFile] = project.symbolIdToFiles(symbol.id); const symbolFileResolvedName = symbolFile?.resolvedNames.get(symbol.id); let name = ensureValidIdentifier(symbolFileResolvedName ?? symbol.name); - if (file.resolvedNames.hasValue(name)) { - name = this.getUniqueName(name, file.resolvedNames); + const conflictId = file.resolvedNames.getKey(name); + if (conflictId !== undefined) { + const conflictSymbol = project.symbols.get(conflictId); + if ( + (conflictSymbol?.meta?.kind === 'type' && + symbol.meta?.kind === 'type') || + (conflictSymbol?.meta?.kind !== 'type' && symbol.meta?.kind !== 'type') + ) { + name = this.getUniqueName(name, file.resolvedNames); + } } file.resolvedNames.set(symbol.id, name); return name; diff --git a/packages/openapi-ts/src/ir/__tests__/graph.test.ts b/packages/openapi-ts/src/ir/__tests__/graph.test.ts new file mode 100644 index 0000000000..417dbf4230 --- /dev/null +++ b/packages/openapi-ts/src/ir/__tests__/graph.test.ts @@ -0,0 +1,326 @@ +import { describe, expect, it } from 'vitest'; + +import type { Graph } from '../../openApi/shared/utils/graph'; +import { buildGraph } from '../../openApi/shared/utils/graph'; +import type { IrTopLevelKind } from '../graph'; +import { matchIrTopLevelPointer, walkTopological } from '../graph'; + +// simple logger stub for buildGraph +const loggerStub = { + timeEvent: () => ({ timeEnd: () => {} }), +} as any; + +describe('matchIrTopLevelPointer', () => { + const cases: Array< + [ + string, + IrTopLevelKind | undefined, + { kind?: IrTopLevelKind; matched: boolean }, + ] + > = [ + ['#/components/schemas/Foo', undefined, { kind: 'schema', matched: true }], + ['#/components/schemas/Foo', 'schema', { kind: 'schema', matched: true }], + ['#/components/schemas/Foo', 'parameter', { matched: false }], + [ + '#/components/parameters/Bar', + undefined, + { kind: 'parameter', matched: true }, + ], + [ + '#/components/parameters/Bar', + 'parameter', + { kind: 'parameter', matched: true }, + ], + ['#/components/parameters/Bar', 'schema', { matched: false }], + [ + '#/components/requestBodies/Baz', + undefined, + { kind: 'requestBody', matched: true }, + ], + [ + '#/components/requestBodies/Baz', + 'requestBody', + { kind: 'requestBody', matched: true }, + ], + ['#/components/requestBodies/Baz', 'schema', { matched: false }], + ['#/servers/0', undefined, { kind: 'server', matched: true }], + ['#/servers/foo', undefined, { kind: 'server', matched: true }], + ['#/paths/~1users/get', undefined, { kind: 'operation', matched: true }], + ['#/paths/~1users/post', 'operation', { kind: 'operation', matched: true }], + ['#/webhooks/foo/get', undefined, { kind: 'webhook', matched: true }], + ['#/webhooks/foo/patch', 'webhook', { kind: 'webhook', matched: true }], + ['#/not/a/top/level', undefined, { matched: false }], + ['#/components/unknown/Foo', undefined, { matched: false }], + ]; + + for (const [pointer, kind, expected] of cases) { + it(`matches ${pointer} with kind=${kind}`, () => { + const result = matchIrTopLevelPointer(pointer, kind as IrTopLevelKind); + expect(result.matched).toBe(expected.matched); + if (expected.matched) { + expect(result.kind).toBe(expected.kind); + } else { + expect(result.kind).toBeUndefined(); + } + }); + } +}); + +describe('walkTopological', () => { + const makeGraph = ( + deps: Array<[string, Array]>, + nodes: Array, + ) => { + const nodeDependencies = new Map>(); + const subtreeDependencies = new Map>(); + const reverseNodeDependencies = new Map>(); + const nodesMap = new Map(); + + for (const name of nodes) { + nodesMap.set(name, { key: null, node: {}, parentPointer: null }); + } + + for (const [from, toList] of deps) { + const s = new Set(toList); + nodeDependencies.set(from, s); + subtreeDependencies.set(from, new Set(toList)); + for (const to of toList) { + if (!reverseNodeDependencies.has(to)) + reverseNodeDependencies.set(to, new Set()); + reverseNodeDependencies.get(to)!.add(from); + } + } + + return { + nodeDependencies, + nodes: nodesMap, + reverseNodeDependencies, + subtreeDependencies, + transitiveDependencies: new Map>(), + } as unknown as Graph; + }; + + it('walks nodes in topological order for a simple acyclic graph', () => { + // Graph: A -> B -> C + const graph = makeGraph( + [ + ['A', ['B']], + ['B', ['C']], + ], + ['A', 'B', 'C'], + ); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.indexOf('C')).toBeLessThan(order.indexOf('B')); + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); + expect(order).toHaveLength(3); + }); + + it('walks nodes in topological order for multiple roots', () => { + // Graph: A -> B, C -> D + const graph = makeGraph( + [ + ['A', ['B']], + ['C', ['D']], + ], + ['A', 'B', 'C', 'D'], + ); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); + expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); + expect(order).toHaveLength(4); + }); + + it('walks nodes in topological order for a disconnected graph', () => { + // Graph: A -> B, C (no deps), D (no deps) + const graph = makeGraph([['A', ['B']]], ['A', 'B', 'C', 'D']); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); + expect(order).toHaveLength(4); + expect(order).toContain('C'); + expect(order).toContain('D'); + }); + + it('walks nodes in topological order for a diamond dependency', () => { + // Graph: A + // / \ + // B C + // \ / + // D + const graph = makeGraph( + [ + ['A', ['B', 'C']], + ['B', ['D']], + ['C', ['D']], + ], + ['A', 'B', 'C', 'D'], + ); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.indexOf('D')).toBeLessThan(order.indexOf('B')); + expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); + expect(order.indexOf('C')).toBeLessThan(order.indexOf('A')); + expect(order).toHaveLength(4); + }); + + it('walks nodes in topological order for a long chain', () => { + // Graph: A -> B -> C -> D -> E + const graph = makeGraph( + [ + ['A', ['B']], + ['B', ['C']], + ['C', ['D']], + ['D', ['E']], + ], + ['A', 'B', 'C', 'D', 'E'], + ); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.indexOf('E')).toBeLessThan(order.indexOf('D')); + expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); + expect(order.indexOf('C')).toBeLessThan(order.indexOf('B')); + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); + expect(order).toHaveLength(5); + }); + + it('walks all nodes, including cycles', () => { + // Graph: A <-> B (cycle), C (no deps) + const graph = makeGraph( + [ + ['A', ['B']], + ['B', ['A']], + ], + ['A', 'B', 'C'], + ); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.sort()).toEqual(['A', 'B', 'C']); + }); + + it('matches ordering for validators-circular-ref spec', async () => { + const specModule = await import( + '../../../../../specs/3.1.x/validators-circular-ref.json' + ); + const spec = specModule.default ?? specModule; + const { graph } = buildGraph(spec, loggerStub); + + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + + const foo = '#/components/schemas/Foo'; + const bar = '#/components/schemas/Bar'; + const baz = '#/components/schemas/Baz'; + const qux = '#/components/schemas/Qux'; + + // Bar should come before Foo because Foo depends on Bar + expect(order.indexOf(bar)).toBeLessThan(order.indexOf(foo)); + + // Baz and Qux form a mutual $ref cycle; both must be present + expect(order).toContain(baz); + expect(order).toContain(qux); + }); + + it('prefers schema group before parameter when safe (default)', () => { + // parameter then schema in declaration order, no deps -> schema should move before parameter + const param = '#/components/parameters/P'; + const schema = '#/components/schemas/A'; + const nodes = [param, schema]; + const graph = makeGraph([], nodes); + + const order: Array = []; + walkTopological(graph, (p) => order.push(p)); + expect(order.indexOf(schema)).toBeLessThan(order.indexOf(param)); + }); + + it('does not apply preferGroups when it would violate dependencies (fallback)', () => { + // declaration order: param, schema; schema depends on param -> cannot move before param + const param = '#/components/parameters/P'; + const schema = '#/components/schemas/S'; + const nodes = [param, schema]; + const nodeDependencies = new Map>(); + nodeDependencies.set(schema, new Set([param])); + const subtreeDependencies = new Map>(); + const reverseNodeDependencies = new Map>(); + const nodesMap = new Map(); + for (const n of nodes) + nodesMap.set(n, { key: null, node: {}, parentPointer: null }); + const graph = { + nodeDependencies, + nodes: nodesMap, + reverseNodeDependencies, + subtreeDependencies, + transitiveDependencies: new Map>(), + } as unknown as Graph; + + const order: Array = []; + walkTopological(graph, (p) => order.push(p)); + // schema depends on param so param must remain before schema + expect(order.indexOf(param)).toBeLessThan(order.indexOf(schema)); + }); + + it('ignores self-dependencies when ordering', () => { + // Foo has self-ref only, Bar references Foo -> Foo should come before Bar + const foo = '#/components/schemas/Foo'; + const bar = '#/components/schemas/Bar'; + const nodes = [foo, bar]; + const nodeDependencies = new Map>(); + nodeDependencies.set(foo, new Set([foo])); + nodeDependencies.set(bar, new Set([foo])); + + const nodesMap = new Map(); + for (const n of nodes) + nodesMap.set(n, { key: null, node: {}, parentPointer: null }); + + const graph = { + nodeDependencies, + nodes: nodesMap, + reverseNodeDependencies: new Map>(), + subtreeDependencies: new Map>(), + transitiveDependencies: new Map>(), + } as unknown as Graph; + + const order: Array = []; + walkTopological(graph, (p) => order.push(p)); + // Foo is a dependency of Bar, so Foo should come before Bar + expect(order.indexOf(foo)).toBeLessThan(order.indexOf(bar)); + }); + + it('uses subtreeDependencies when nodeDependencies are absent', () => { + const parent = '#/components/schemas/Parent'; + const child = '#/components/schemas/Child'; + const nodes = [parent, child]; + const nodeDependencies = new Map>(); + const subtreeDependencies = new Map>(); + subtreeDependencies.set(parent, new Set([child])); + + const nodesMap = new Map(); + for (const n of nodes) + nodesMap.set(n, { key: null, node: {}, parentPointer: null }); + + const graph = { + nodeDependencies, + nodes: nodesMap, + reverseNodeDependencies: new Map>(), + subtreeDependencies, + transitiveDependencies: new Map>(), + } as unknown as Graph; + + const order: Array = []; + walkTopological(graph, (p) => order.push(p)); + expect(order.indexOf(child)).toBeLessThan(order.indexOf(parent)); + }); + + it('preserves declaration order for equal-priority items (stability)', () => { + const a = '#/components/schemas/A'; + const b = '#/components/schemas/B'; + const c = '#/components/schemas/C'; + const nodes = [a, b, c]; + const graph = makeGraph([], nodes); + const order: Array = []; + walkTopological(graph, (p) => order.push(p)); + expect(order).toEqual(nodes); + }); +}); diff --git a/packages/openapi-ts/src/ir/context.ts b/packages/openapi-ts/src/ir/context.ts index 3fb4d8f90f..b7abc6c925 100644 --- a/packages/openapi-ts/src/ir/context.ts +++ b/packages/openapi-ts/src/ir/context.ts @@ -3,6 +3,7 @@ import { Project } from '@hey-api/codegen-core'; import type { Package } from '../config/utils/package'; import { packageFactory } from '../config/utils/package'; import { TypeScriptRenderer } from '../generate/renderer'; +import type { Graph } from '../openApi/shared/utils/graph'; import { buildName } from '../openApi/shared/utils/name'; import type { PluginConfigMap } from '../plugins/config'; import { PluginInstance } from '../plugins/shared/utils/instance'; @@ -22,6 +23,10 @@ export class IRContext = any> { * The code generation project instance used to manage files, symbols, */ gen: Project; + /** + * The dependency graph built from the intermediate representation. + */ + graph: Graph | undefined; /** * Intermediate representation model obtained from `spec`. */ diff --git a/packages/openapi-ts/src/ir/graph.ts b/packages/openapi-ts/src/ir/graph.ts new file mode 100644 index 0000000000..ee62ac28ff --- /dev/null +++ b/packages/openapi-ts/src/ir/graph.ts @@ -0,0 +1,261 @@ +import type { Graph, NodeInfo } from '../openApi/shared/utils/graph'; +import { MinHeap } from '../utils/minHeap'; + +type KindPriority = Record; +type PreferGroups = ReadonlyArray; +type PriorityFn = (pointer: string) => number; + +/** + * Walks the nodes of the graph in topological order (dependencies before dependents). + * Calls the callback for each node pointer in order. + * Nodes in cycles are grouped together and emitted in arbitrary order within the group. + * + * @param graph - The dependency graph + * @param callback - Function to call for each node pointer + */ +export const walkTopological = ( + graph: Graph, + callback: (pointer: string, nodeInfo: NodeInfo) => void, + options?: { + preferGroups?: PreferGroups; + priority?: PriorityFn; + }, +) => { + // Stable Kahn's algorithm that respects declaration order as a tiebreaker. + const pointers = Array.from(graph.nodes.keys()); + // Base insertion order + const baseIndex = new Map(); + pointers.forEach((pointer, index) => baseIndex.set(pointer, index)); + + // Composite decl index: group priority then base insertion order + const declIndex = new Map(); + const priorityFn = options?.priority ?? defaultPriorityFn; + for (const pointer of pointers) { + const group = priorityFn(pointer) ?? 10; + const composite = group * 1_000_000 + (baseIndex.get(pointer) ?? 0); + declIndex.set(pointer, composite); + } + + // Build dependency sets for each pointer (prefer subtreeDependencies, fall back to nodeDependencies) + const depsOf = new Map>(); + for (const pointer of pointers) { + const raw = + graph.subtreeDependencies?.get(pointer) ?? + graph.nodeDependencies?.get(pointer) ?? + new Set(); + const filtered = new Set(); + for (const rawPointer of raw) { + if (rawPointer === pointer) continue; // ignore self-dependencies for ordering + if (graph.nodes.has(rawPointer)) { + filtered.add(rawPointer); + } + } + depsOf.set(pointer, filtered); + } + + // Build inDegree and dependents adjacency + const inDegree = new Map(); + const dependents = new Map>(); + for (const pointer of pointers) { + inDegree.set(pointer, 0); + } + for (const [pointer, deps] of depsOf) { + inDegree.set(pointer, deps.size); + for (const d of deps) { + if (!dependents.has(d)) { + dependents.set(d, new Set()); + } + dependents.get(d)!.add(pointer); + } + } + + // Helper to sort pointers by declaration order + const sortByDecl = (arr: Array) => + arr.sort((a, b) => declIndex.get(a)! - declIndex.get(b)!); + + // Initialize queue with zero-inDegree nodes in declaration order + // Use a small binary min-heap prioritized by declaration index to avoid repeated full sorts. + const heap = new MinHeap(declIndex); + for (const pointer of pointers) { + if ((inDegree.get(pointer) ?? 0) === 0) { + heap.push(pointer); + } + } + + const emitted = new Set(); + const order: Array = []; + + while (!heap.isEmpty()) { + const cur = heap.pop()!; + if (emitted.has(cur)) continue; + emitted.add(cur); + order.push(cur); + + const deps = dependents.get(cur); + if (!deps) continue; + + for (const dep of deps) { + const v = (inDegree.get(dep) ?? 0) - 1; + inDegree.set(dep, v); + if (v === 0) { + heap.push(dep); + } + } + } + + // emit remaining nodes (cycles) in declaration order + const remaining = pointers.filter((pointer) => !emitted.has(pointer)); + sortByDecl(remaining); + for (const pointer of remaining) { + emitted.add(pointer); + order.push(pointer); + } + + // prefer specified groups when safe + let finalOrder = order; + const preferGroups = options?.preferGroups ?? defaultPreferGroups; + if (preferGroups && preferGroups.length > 0) { + // build group priority map (lower = earlier) + const groupPriority = new Map(); + for (let i = 0; i < preferGroups.length; i++) { + const k = preferGroups[i]; + if (k) { + groupPriority.set(k, i); + } + } + + const getGroup: PriorityFn = (pointer) => { + const result = matchIrTopLevelPointer(pointer); + if (result.matched) { + return groupPriority.has(result.kind) + ? groupPriority.get(result.kind)! + : preferGroups.length; + } + return preferGroups.length; + }; + + // proposed order: sort by (groupPriority, originalIndex) + const proposed = [...order].sort((a, b) => { + const ga = getGroup(a); + const gb = getGroup(b); + return ga !== gb ? ga - gb : order.indexOf(a) - order.indexOf(b); + }); + + // Build quick lookup of original index and proposed index + const proposedIndex = new Map(); + for (let i = 0; i < proposed.length; i++) { + proposedIndex.set(proposed[i]!, i); + } + + // Micro-optimization: only validate edges where group(dep) > group(node) + const violated = (() => { + for (const [node, deps] of depsOf) { + for (const dep of deps) { + const gDep = getGroup(dep); + const gNode = getGroup(node); + if (gDep <= gNode) continue; // not a crossing edge, cannot be violated by grouping + const pDep = proposedIndex.get(dep)!; + const pNode = proposedIndex.get(node)!; + if (pDep >= pNode) { + return true; + } + } + } + return false; + })(); + + if (!violated) { + finalOrder = proposed; + } + } + + // Finally, call back in final order + for (const pointer of finalOrder) { + callback(pointer, graph.nodes.get(pointer)!); + } +}; + +export const irTopLevelKinds = [ + 'operation', + 'parameter', + 'requestBody', + 'schema', + 'server', + 'webhook', +] as const; + +export type IrTopLevelKind = (typeof irTopLevelKinds)[number]; + +export type IrTopLevelPointerMatch = + | { kind: IrTopLevelKind; matched: true } + | { kind?: undefined; matched: false }; + +/** + * Checks if a pointer matches a known top-level IR component (schema, parameter, etc) and returns match info. + * + * @param pointer - The IR pointer string (e.g. '#/components/schemas/Foo') + * @param kind - (Optional) The component kind to check + * @returns { matched: true, kind: IrTopLevelKind } | { matched: false } - Whether it matched, and the matched kind if so + */ +export const matchIrTopLevelPointer = ( + pointer: string, + kind?: IrTopLevelKind, +): IrTopLevelPointerMatch => { + const patterns: Record = { + operation: + /^#\/paths\/[^/]+\/(get|put|post|delete|options|head|patch|trace)$/, + parameter: /^#\/components\/parameters\/[^/]+$/, + requestBody: /^#\/components\/requestBodies\/[^/]+$/, + schema: /^#\/components\/schemas\/[^/]+$/, + server: /^#\/servers\/(\d+|[^/]+)$/, + webhook: + /^#\/webhooks\/[^/]+\/(get|put|post|delete|options|head|patch|trace)$/, + }; + if (kind) { + return patterns[kind].test(pointer) + ? { kind, matched: true } + : { matched: false }; + } + for (const key of Object.keys(patterns)) { + const kind = key as IrTopLevelKind; + if (patterns[kind].test(pointer)) { + return { kind, matched: true }; + } + } + return { matched: false }; +}; + +// default grouping preference (earlier groups emitted first when safe) +export const defaultPreferGroups = [ + 'schema', + 'parameter', + 'requestBody', + 'operation', + 'server', + 'webhook', +] satisfies PreferGroups; + +// default group priority (lower = earlier) +// built from `defaultPreferGroups` so the priority order stays in sync with the prefer-groups array. +const defaultKindPriority: KindPriority = (() => { + const partial: Partial = {}; + for (let i = 0; i < defaultPreferGroups.length; i++) { + const k = defaultPreferGroups[i]; + if (k) partial[k] = i; + } + // Ensure all known kinds exist in the map (fall back to a high index). + for (const k of irTopLevelKinds) { + if (partial[k] === undefined) { + partial[k] = defaultPreferGroups.length; + } + } + return partial as KindPriority; +})(); + +const defaultPriorityFn: PriorityFn = (pointer) => { + const result = matchIrTopLevelPointer(pointer); + if (result.matched) { + return defaultKindPriority[result.kind] ?? 10; + } + return 10; +}; diff --git a/packages/openapi-ts/src/ir/types.d.ts b/packages/openapi-ts/src/ir/types.d.ts index 309053d347..1186471988 100644 --- a/packages/openapi-ts/src/ir/types.d.ts +++ b/packages/openapi-ts/src/ir/types.d.ts @@ -249,13 +249,6 @@ interface IRSchemaObject * properties altogether. */ additionalProperties?: IRSchemaObject | false; - /** - * If this schema is a $ref and is circular (points to itself or is in the current resolution stack), - * this flag is set to true. - * - * @default undefined - */ - circular?: boolean; /** * Any string value is accepted as `format`. */ @@ -281,7 +274,6 @@ interface IRSchemaObject * When type is `object`, `properties` will contain a map of its properties. */ properties?: Record; - /** * The names of `properties` can be validated against a schema, irrespective * of their values. This can be useful if you don't want to enforce specific diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/parameter.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/parameter.ts index cc1117d460..3e51a63049 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/parameter.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/parameter.ts @@ -146,7 +146,6 @@ const parameterToIrParameter = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }), style, diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts index 6b03b24f1a..0208de48b3 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts @@ -535,7 +535,6 @@ const parseRef = ({ const isComponentsRef = schema.$ref.startsWith('#/definitions/'); if (!isComponentsRef) { if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -545,7 +544,6 @@ const parseRef = ({ state, }); state.$ref = originalRef; - state.refStack.pop(); return irSchema; } // Fallback to preserving the ref if circular @@ -562,15 +560,7 @@ const parseRef = ({ '#/components/schemas/$1', ); - if (state.refStack.includes(schema.$ref)) { - if (state.refStack[0] === schema.$ref) { - state.circularRef = schema.$ref; - } - irSchema.circular = true; - } - if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -579,11 +569,7 @@ const parseRef = ({ schema: refSchema, state, }); - if (state.circularRef && state.refStack[0] === state.circularRef) { - irSchema.circular = true; - } state.$ref = originalRef; - state.refStack.pop(); } return irSchema; @@ -771,7 +757,6 @@ export const schemaToIrSchema = ({ if (!state) { state = { circularReferenceTracker: new Set(), - refStack: [], }; } @@ -838,7 +823,6 @@ export const parseSchema = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }); }; diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts index 75f1a5fa6d..56307923e5 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts @@ -150,7 +150,6 @@ const parameterToIrParameter = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }), style, diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/requestBody.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/requestBody.ts index 2aa311f4dd..3f45e7e1a5 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/requestBody.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/requestBody.ts @@ -32,7 +32,6 @@ const requestBodyToIrRequestBody = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }), }; diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts index b80ca1949d..e5679f6a6e 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts @@ -741,7 +741,6 @@ const parseRef = ({ const isComponentsRef = schema.$ref.startsWith('#/components/'); if (!isComponentsRef) { if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -751,7 +750,6 @@ const parseRef = ({ state, }); state.$ref = originalRef; - state.refStack.pop(); return irSchema; } // Fallback to preserving the ref if circular @@ -763,15 +761,7 @@ const parseRef = ({ // but the suspicion is this comes from `@hey-api/json-schema-ref-parser` irSchema.$ref = decodeURI(schema.$ref); - if (state.refStack.includes(schema.$ref)) { - if (state.refStack[0] === schema.$ref) { - state.circularRef = schema.$ref; - } - irSchema.circular = true; - } - if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -780,11 +770,7 @@ const parseRef = ({ schema: refSchema, state, }); - if (state.circularRef && state.refStack[0] === state.circularRef) { - irSchema.circular = true; - } state.$ref = originalRef; - state.refStack.pop(); } return irSchema; @@ -972,7 +958,6 @@ export const schemaToIrSchema = ({ if (!state) { state = { circularReferenceTracker: new Set(), - refStack: [], }; } @@ -1055,7 +1040,6 @@ export const parseSchema = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }); }; diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts index 13748445d2..be0c875106 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts @@ -143,7 +143,6 @@ const parameterToIrParameter = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }), style, diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/requestBody.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/requestBody.ts index 2aa311f4dd..3f45e7e1a5 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/requestBody.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/requestBody.ts @@ -32,7 +32,6 @@ const requestBodyToIrRequestBody = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }), }; diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts index b2ac1c3f7a..444fc59841 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts @@ -800,7 +800,6 @@ const parseRef = ({ const isComponentsRef = schema.$ref.startsWith('#/components/'); if (!isComponentsRef) { if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -810,7 +809,6 @@ const parseRef = ({ state, }); state.$ref = originalRef; - state.refStack.pop(); return irSchema; } // Fallback to preserving the ref if circular @@ -824,15 +822,7 @@ const parseRef = ({ // but the suspicion is this comes from `@hey-api/json-schema-ref-parser` irRefSchema.$ref = decodeURI(schema.$ref); - if (state.refStack.includes(schema.$ref)) { - if (state.refStack[0] === schema.$ref) { - state.circularRef = schema.$ref; - } - irSchema.circular = true; - } - if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -841,11 +831,7 @@ const parseRef = ({ schema: refSchema, state, }); - if (state.circularRef && state.refStack[0] === state.circularRef) { - irSchema.circular = true; - } state.$ref = originalRef; - state.refStack.pop(); } const schemaItems: Array = []; @@ -1054,7 +1040,6 @@ export const schemaToIrSchema = ({ if (!state) { state = { circularReferenceTracker: new Set(), - refStack: [], }; } @@ -1137,7 +1122,6 @@ export const parseSchema = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }); }; diff --git a/packages/openapi-ts/src/openApi/common/parser/__tests__/type.test.ts b/packages/openapi-ts/src/openApi/common/parser/__tests__/type.test.ts index bc2d9426dc..62de82c5e9 100644 --- a/packages/openapi-ts/src/openApi/common/parser/__tests__/type.test.ts +++ b/packages/openapi-ts/src/openApi/common/parser/__tests__/type.test.ts @@ -9,8 +9,8 @@ vi.mock('../../../../utils/config', () => { plugins: { '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', diff --git a/packages/openapi-ts/src/openApi/shared/graph/meta.ts b/packages/openapi-ts/src/openApi/shared/graph/meta.ts index 4d96e0940d..326ab3b69c 100644 --- a/packages/openapi-ts/src/openApi/shared/graph/meta.ts +++ b/packages/openapi-ts/src/openApi/shared/graph/meta.ts @@ -67,10 +67,10 @@ export const buildResourceMetadata = ( const getDependencies = (pointer: string): Set => { const dependencies = new Set(); - const nodeDeps = graph.allDependencies.get(pointer); - if (nodeDeps?.size) { - for (const dep of nodeDeps) { - const path = jsonPointerToPath(dep); + const nodeDependencies = graph.transitiveDependencies.get(pointer); + if (nodeDependencies?.size) { + for (const dependency of nodeDependencies) { + const path = jsonPointerToPath(dependency); const type = path[path.length - 2]; const name = path[path.length - 1]; if (type && name) { diff --git a/packages/openapi-ts/src/openApi/shared/types/schema.d.ts b/packages/openapi-ts/src/openApi/shared/types/schema.d.ts index 5b5bb9ecd1..f52469bb96 100644 --- a/packages/openapi-ts/src/openApi/shared/types/schema.d.ts +++ b/packages/openapi-ts/src/openApi/shared/types/schema.d.ts @@ -4,17 +4,9 @@ export interface SchemaState { * from the OpenAPI specification. */ $ref?: string; - /** - * If a reference is detected as circular, this will hold the $ref string. - * This is used to back-propagate circular reference information to the - * original schema that started the circular reference chain. - */ - circularRef?: string; /** * Set of $refs currently being resolved that are circular. This is used to * avoid infinite loops when resolving schemas with circular references. - * - * @deprecated Use `refStack` instead. */ circularReferenceTracker: Set; /** @@ -24,11 +16,6 @@ export interface SchemaState { * properties from other schemas in the composition. */ inAllOf?: boolean; - /** - * Stack of $refs currently being resolved. This is used to detect circular - * references and avoid infinite loops. - */ - refStack: Array; } export type SchemaWithRequired< diff --git a/packages/openapi-ts/src/openApi/shared/utils/__tests__/graph.test.ts b/packages/openapi-ts/src/openApi/shared/utils/__tests__/graph.test.ts new file mode 100644 index 0000000000..e9d1d9d304 --- /dev/null +++ b/packages/openapi-ts/src/openApi/shared/utils/__tests__/graph.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from 'vitest'; + +import { buildGraph } from '../graph'; + +// simple logger stub for buildGraph +const loggerStub = { + timeEvent: () => ({ timeEnd: () => {} }), +} as any; + +describe('buildGraph', () => { + it('computes referenced and transitive dependencies for validators-circular-ref.json', async () => { + const mod = await import( + '../../../../../../../specs/3.1.x/validators-circular-ref.json' + ); + const spec = (mod as any).default ?? mod; + + const { graph } = buildGraph(spec, loggerStub); + + const foo = '#/components/schemas/Foo'; + const bar = '#/components/schemas/Bar'; + const baz = '#/components/schemas/Baz'; + const qux = '#/components/schemas/Qux'; + + // Foo has a child property that $ref's Bar, so Foo should have Bar in subtreeDependencies + expect(graph.subtreeDependencies.get(foo)).toBeDefined(); + expect(Array.from(graph.subtreeDependencies.get(foo)!).sort()).toEqual( + [bar].sort(), + ); + + // Foo transitively depends on Bar (via the child), so transitiveDependencies should include Bar + expect(graph.transitiveDependencies.get(foo)).toBeDefined(); + expect(Array.from(graph.transitiveDependencies.get(foo)!).sort()).toEqual( + [bar].sort(), + ); + + // Bar references itself via an array item; Bar should reference Bar + expect(Array.from(graph.subtreeDependencies.get(bar)!).sort()).toEqual( + [bar].sort(), + ); + + // Baz and Qux form a mutual $ref cycle; each should reference the other in subtreeDependencies + expect(Array.from(graph.subtreeDependencies.get(baz)!).sort()).toEqual( + [qux].sort(), + ); + expect(Array.from(graph.subtreeDependencies.get(qux)!).sort()).toEqual( + [baz].sort(), + ); + + // Qux node should exist and have a direct dependency to Baz (node-level $ref) + expect(graph.nodes.has(qux)).toBe(true); + expect(graph.nodeDependencies.get(qux)).toBeDefined(); + expect(Array.from(graph.nodeDependencies.get(qux)!).sort()).toEqual( + [baz].sort(), + ); + + // Qux transitive deps should include Baz (and vice-versa because of the cycle) + expect(graph.transitiveDependencies.get(qux)).toBeDefined(); + expect(Array.from(graph.transitiveDependencies.get(qux)!).sort()).toEqual( + [baz].sort(), + ); + + // Reverse dependencies should reflect the mutual references + expect(graph.reverseNodeDependencies.get(qux)).toBeDefined(); + expect(Array.from(graph.reverseNodeDependencies.get(qux)!).sort()).toEqual( + [baz].sort(), + ); + expect(Array.from(graph.reverseNodeDependencies.get(baz)!).sort()).toEqual( + [qux].sort(), + ); + }); + + it('handles a small hand-constructed tree with child-level $ref', () => { + const spec = { + components: { + schemas: { + A: { + properties: { p: { $ref: '#/components/schemas/B' } }, + type: 'object', + }, + B: { type: 'object' }, + }, + }, + }; + + const { graph } = buildGraph(spec, loggerStub); + + const a = '#/components/schemas/A'; + const b = '#/components/schemas/B'; + + expect(Array.from(graph.subtreeDependencies.get(a)!).sort()).toEqual( + [b].sort(), + ); + expect(Array.from(graph.transitiveDependencies.get(a)!).sort()).toEqual( + [b].sort(), + ); + // reverseNodeDependencies should record that b is referenced by the property child as well + expect(graph.reverseNodeDependencies.get(b)).toBeDefined(); + expect( + Array.from(graph.reverseNodeDependencies.get(b)!).some((p) => + p.startsWith(a), + ), + ).toBe(true); + }); +}); diff --git a/packages/openapi-ts/src/openApi/shared/utils/graph.ts b/packages/openapi-ts/src/openApi/shared/utils/graph.ts index d00f019528..562d7b8ebf 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/graph.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/graph.ts @@ -20,7 +20,7 @@ export type Scope = 'normal' | 'read' | 'write'; * @property scopes - The set of access scopes for this node, if any. Optional. * @property tags - The set of tags for this node, if any. Optional. */ -type NodeInfo = { +export type NodeInfo = { /** Whether the node is deprecated. Optional. */ deprecated?: boolean; /** The property name or array index in the parent, or null for root. */ @@ -38,22 +38,37 @@ type NodeInfo = { /** * The main graph structure for OpenAPI node analysis. * - * @property dependencies - For each node, the set of normalized JSON Pointers it references via $ref. + * @property nodeDependencies - For each node with at least one dependency, the set of normalized JSON Pointers it references via $ref. Nodes with no dependencies are omitted. * @property nodes - Map from normalized JSON Pointer to NodeInfo for every node in the spec. - * @property reverseDependencies - For each node, the set of nodes that reference it via $ref. + * @property reverseNodeDependencies - For each node with at least one dependent, the set of nodes that reference it via $ref. Nodes with no dependents are omitted. */ export type Graph = { + /** + * For each node with at least one dependency, the set of normalized JSON Pointers it references via $ref. + * Nodes with no dependencies are omitted from this map. + */ + nodeDependencies: Map>; + /** + * Map from normalized JSON Pointer to NodeInfo for every node in the spec. + */ + nodes: Map; + /** + * For each node with at least one dependent, the set of nodes that reference it via $ref. + * Nodes with no dependents are omitted from this map. + */ + reverseNodeDependencies: Map>; + /** + * For each node, the set of direct $ref targets that appear anywhere inside the node's + * subtree (the node itself and its children). This is populated during graph construction + * and is used to compute top-level dependency relationships where $ref may be attached to + * child pointers instead of the parent. + */ + subtreeDependencies: Map>; /** * For each node, the set of all (transitive) normalized JSON Pointers it references via $ref anywhere in its subtree. * This includes both direct and indirect dependencies, making it useful for filtering, codegen, and tree-shaking. */ - allDependencies: Map>; - /** For each node, the set of normalized JSON Pointers it references via $ref. */ - dependencies: Map>; - /** Map from normalized JSON Pointer to NodeInfo for every node in the spec. */ - nodes: Map; - /** For each node, the set of nodes that reference it via $ref. */ - reverseDependencies: Map>; + transitiveDependencies: Map>; }; /** @@ -76,15 +91,20 @@ export const annotateChildScopes = (nodes: Graph['nodes']): void => { }; interface Cache { - allDependencies: Map>; - childDependencies: Map>; parentToChildren: Map>; + subtreeDependencies: Map>; + transitiveDependencies: Map>; } +type PointerDependenciesResult = { + subtreeDependencies: Set; + transitiveDependencies: Set; +}; + /** * Recursively collects all $ref dependencies in the subtree rooted at `pointer`. */ -const collectAllDependenciesForPointer = ({ +const collectPointerDependencies = ({ cache, graph, pointer, @@ -94,67 +114,94 @@ const collectAllDependenciesForPointer = ({ graph: Graph; pointer: string; visited: Set; -}): Set => { - const cached = cache.allDependencies.get(pointer); +}): PointerDependenciesResult => { + const cached = cache.transitiveDependencies.get(pointer); if (cached) { - return cached; + return { + subtreeDependencies: cache.subtreeDependencies.get(pointer)!, + transitiveDependencies: cached, + }; } if (visited.has(pointer)) { - return new Set(); + return { + subtreeDependencies: new Set(), + transitiveDependencies: new Set(), + }; } - visited.add(pointer); const nodeInfo = graph.nodes.get(pointer); if (!nodeInfo) { - return new Set(); + return { + subtreeDependencies: new Set(), + transitiveDependencies: new Set(), + }; } - const allDependencies = new Set(); + const transitiveDependencies = new Set(); + const subtreeDependencies = new Set(); // Add direct $ref dependencies for this node // (from the dependencies map, or by checking nodeInfo.node directly) // We'll use the dependencies map for consistency: - const dependencies = graph.dependencies.get(pointer); - if (dependencies) { - for (const depPointer of dependencies) { - allDependencies.add(depPointer); + const nodeDependencies = graph.nodeDependencies.get(pointer); + if (nodeDependencies) { + for (const depPointer of nodeDependencies) { + transitiveDependencies.add(depPointer); + subtreeDependencies.add(depPointer); // Recursively collect dependencies of the referenced node - const transitiveDependencies = collectAllDependenciesForPointer({ + const depResult = collectPointerDependencies({ cache, graph, pointer: depPointer, visited, }); - for (const dep of transitiveDependencies) { - allDependencies.add(dep); + for (const dependency of depResult.transitiveDependencies) { + transitiveDependencies.add(dependency); } } } - // Recursively collect dependencies of all children - const children = cache.parentToChildren.get(pointer); - if (children) { - for (const childPointer of children) { - let transitiveDependencies = cache.childDependencies.get(childPointer); - if (!transitiveDependencies) { - transitiveDependencies = collectAllDependenciesForPointer({ - cache, - graph, - pointer: childPointer, - visited, - }); - cache.childDependencies.set(childPointer, transitiveDependencies); - } - for (const dep of transitiveDependencies) { - allDependencies.add(dep); - } + const children = cache.parentToChildren.get(pointer) ?? []; + for (const childPointer of children) { + let childResult: Partial = { + subtreeDependencies: cache.subtreeDependencies.get(childPointer), + transitiveDependencies: cache.transitiveDependencies.get(childPointer), + }; + if ( + !childResult.subtreeDependencies || + !childResult.transitiveDependencies + ) { + childResult = collectPointerDependencies({ + cache, + graph, + pointer: childPointer, + visited, + }); + cache.transitiveDependencies.set( + childPointer, + childResult.transitiveDependencies!, + ); + cache.subtreeDependencies.set( + childPointer, + childResult.subtreeDependencies!, + ); + } + for (const dependency of childResult.transitiveDependencies!) { + transitiveDependencies.add(dependency); + } + for (const dependency of childResult.subtreeDependencies!) { + subtreeDependencies.add(dependency); } } - cache.allDependencies.set(pointer, allDependencies); - return allDependencies; + cache.transitiveDependencies.set(pointer, transitiveDependencies); + cache.subtreeDependencies.set(pointer, subtreeDependencies); + return { + subtreeDependencies, + transitiveDependencies, + }; }; /** @@ -167,7 +214,7 @@ const collectAllDependenciesForPointer = ({ * - All nodes that reference it via $ref (reverse dependencies) * - Combinator parents (allOf/anyOf/oneOf) if applicable * - * @param graph - The Graph structure containing nodes, dependencies, and reverseDependencies. + * @param graph - The Graph structure containing nodes, dependencies, and reverseNodeDependencies. */ export const propagateScopes = (graph: Graph): void => { const worklist: Set = new Set( @@ -195,9 +242,9 @@ export const propagateScopes = (graph: Graph): void => { if (nodeInfo.parentPointer) { worklist.add(nodeInfo.parentPointer); } - const reverseDependencies = graph.reverseDependencies.get(pointer); - if (reverseDependencies) { - for (const dependentPointer of reverseDependencies) { + const reverseNodeDependencies = graph.reverseNodeDependencies.get(pointer); + if (reverseNodeDependencies) { + for (const dependentPointer of reverseNodeDependencies) { worklist.add(dependentPointer); } } @@ -290,9 +337,9 @@ export const propagateScopes = (graph: Graph): void => { } // Propagate scopes from $ref dependencies - const dependencies = graph.dependencies.get(pointer); - if (dependencies) { - for (const depPointer of dependencies) { + const nodeDependencies = graph.nodeDependencies.get(pointer); + if (nodeDependencies) { + for (const depPointer of nodeDependencies) { const depNode = graph.nodes.get(depPointer); if (depNode?.scopes) { const changed = propagateScopesToNode(depNode, nodeInfo); @@ -384,14 +431,14 @@ export const seedLocalScopes = (nodes: Graph['nodes']): void => { * - All keys in the returned maps are normalized JSON Pointers (RFC 6901, always starting with '#'). * - The `nodes` map allows fast lookup of any node and its parent/key context. * - The `dependencies` map records, for each node, the set of normalized pointers it references via $ref. - * - The `reverseDependencies` map records, for each node, the set of nodes that reference it via $ref. + * - The `reverseNodeDependencies` map records, for each node, the set of nodes that reference it via $ref. * - After construction, all nodes will have their local and propagated scopes annotated. * * @param root The root object (e.g., the OpenAPI spec) * @returns An object with: * - nodes: Map from normalized JSON Pointer string to NodeInfo * - dependencies: Map from normalized JSON Pointer string to Set of referenced normalized JSON Pointers - * - reverseDependencies: Map from normalized JSON Pointer string to Set of referencing normalized JSON Pointers + * - reverseNodeDependencies: Map from normalized JSON Pointer string to Set of referencing normalized JSON Pointers */ export const buildGraph = ( root: unknown, @@ -401,10 +448,11 @@ export const buildGraph = ( } => { const eventBuildGraph = logger.timeEvent('build-graph'); const graph: Graph = { - allDependencies: new Map(), - dependencies: new Map(), + nodeDependencies: new Map(), nodes: new Map(), - reverseDependencies: new Map(), + reverseNodeDependencies: new Map(), + subtreeDependencies: new Map(), + transitiveDependencies: new Map(), }; const walk = ({ @@ -432,10 +480,10 @@ export const buildGraph = ( // If this node has a $ref, record the dependency if ('$ref' in node && typeof node.$ref === 'string') { const refPointer = normalizeJsonPointer(node.$ref); - if (!graph.dependencies.has(pointer)) { - graph.dependencies.set(pointer, new Set()); + if (!graph.nodeDependencies.has(pointer)) { + graph.nodeDependencies.set(pointer, new Set()); } - graph.dependencies.get(pointer)!.add(refPointer); + graph.nodeDependencies.get(pointer)!.add(refPointer); } // Check for tags property (should be an array of strings) if ('tags' in node && node.tags instanceof Array) { @@ -474,9 +522,9 @@ export const buildGraph = ( }); const cache: Cache = { - allDependencies: new Map(), - childDependencies: new Map(), parentToChildren: new Map(), + subtreeDependencies: new Map(), + transitiveDependencies: new Map(), }; for (const [pointer, nodeInfo] of graph.nodes) { @@ -488,12 +536,12 @@ export const buildGraph = ( cache.parentToChildren.get(parent)!.push(pointer); } - for (const [pointerFrom, pointers] of graph.dependencies) { + for (const [pointerFrom, pointers] of graph.nodeDependencies) { for (const pointerTo of pointers) { - if (!graph.reverseDependencies.has(pointerTo)) { - graph.reverseDependencies.set(pointerTo, new Set()); + if (!graph.reverseNodeDependencies.has(pointerTo)) { + graph.reverseNodeDependencies.set(pointerTo, new Set()); } - graph.reverseDependencies.get(pointerTo)!.add(pointerFrom); + graph.reverseNodeDependencies.get(pointerTo)!.add(pointerFrom); } } @@ -502,13 +550,14 @@ export const buildGraph = ( annotateChildScopes(graph.nodes); for (const pointer of graph.nodes.keys()) { - const allDependencies = collectAllDependenciesForPointer({ + const result = collectPointerDependencies({ cache, graph, pointer, visited: new Set(), }); - graph.allDependencies.set(pointer, allDependencies); + graph.transitiveDependencies.set(pointer, result.transitiveDependencies); + graph.subtreeDependencies.set(pointer, result.subtreeDependencies); } eventBuildGraph.timeEnd(); diff --git a/packages/openapi-ts/src/openApi/v3/parser/__tests__/getModel.test.ts b/packages/openapi-ts/src/openApi/v3/parser/__tests__/getModel.test.ts index dbc8979b3c..53461cd762 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/__tests__/getModel.test.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/__tests__/getModel.test.ts @@ -11,8 +11,8 @@ vi.mock('../../../../utils/config', () => { plugins: { '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', diff --git a/packages/openapi-ts/src/plugins/@angular/common/api.ts b/packages/openapi-ts/src/plugins/@angular/common/api.ts index 3cac308504..00e28a0620 100644 --- a/packages/openapi-ts/src/plugins/@angular/common/api.ts +++ b/packages/openapi-ts/src/plugins/@angular/common/api.ts @@ -22,13 +22,13 @@ export type IApi = { * - `Injectable`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@angular/common'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@angular/common/httpRequests.ts b/packages/openapi-ts/src/plugins/@angular/common/httpRequests.ts index 3c60b7f482..7874f75ad0 100644 --- a/packages/openapi-ts/src/plugins/@angular/common/httpRequests.ts +++ b/packages/openapi-ts/src/plugins/@angular/common/httpRequests.ts @@ -28,70 +28,76 @@ const generateAngularClassRequests = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, + operation, + }); - const classes = operationClasses({ - context: plugin.context, - operation, - plugin: sdkPlugin, - }); + const classes = operationClasses({ + context: plugin.context, + operation, + plugin: sdkPlugin, + }); - for (const entry of classes.values()) { - entry.path.forEach((currentClassName, index) => { - if (!requestClasses.has(currentClassName)) { - requestClasses.set(currentClassName, { - className: currentClassName, - classes: new Set(), - methods: new Set(), - nodes: [], - root: !index, - }); - } + for (const entry of classes.values()) { + entry.path.forEach((currentClassName, index) => { + if (!requestClasses.has(currentClassName)) { + requestClasses.set(currentClassName, { + className: currentClassName, + classes: new Set(), + methods: new Set(), + nodes: [], + root: !index, + }); + } - const parentClassName = entry.path[index - 1]; - if (parentClassName && parentClassName !== currentClassName) { - const parentClass = requestClasses.get(parentClassName)!; - parentClass.classes.add(currentClassName); - requestClasses.set(parentClassName, parentClass); - } + const parentClassName = entry.path[index - 1]; + if (parentClassName && parentClassName !== currentClassName) { + const parentClass = requestClasses.get(parentClassName)!; + parentClass.classes.add(currentClassName); + requestClasses.set(parentClassName, parentClass); + } - const isLast = entry.path.length === index + 1; - if (!isLast) { - return; - } + const isLast = entry.path.length === index + 1; + if (!isLast) { + return; + } - const currentClass = requestClasses.get(currentClassName)!; + const currentClass = requestClasses.get(currentClassName)!; - const requestMethodName = - plugin.config.httpRequests.methodNameBuilder(operation); + const requestMethodName = + plugin.config.httpRequests.methodNameBuilder(operation); - if (currentClass.methods.has(requestMethodName)) { - return; - } + if (currentClass.methods.has(requestMethodName)) { + return; + } - const methodNode = generateAngularRequestMethod({ - isRequiredOptions, - methodName: requestMethodName, - operation, - plugin, - }); + const methodNode = generateAngularRequestMethod({ + isRequiredOptions, + methodName: requestMethodName, + operation, + plugin, + }); - if (!currentClass.nodes.length) { - currentClass.nodes.push(methodNode); - } else { - // @ts-expect-error - currentClass.nodes.push(tsc.identifier({ text: '\n' }), methodNode); - } + if (!currentClass.nodes.length) { + currentClass.nodes.push(methodNode); + } else { + // @ts-expect-error + currentClass.nodes.push(tsc.identifier({ text: '\n' }), methodNode); + } - currentClass.methods.add(requestMethodName); - requestClasses.set(currentClassName, currentClass); - }); - } - }); + currentClass.methods.add(requestMethodName); + requestClasses.set(currentClassName, currentClass); + }); + } + }, + { + order: 'declarations', + }, + ); const generateClass = (currentClass: AngularRequestClassEntry) => { if (generatedClasses.has(currentClass.className)) { @@ -127,7 +133,7 @@ const generateAngularClassRequests = ({ } const symbolInjectable = plugin.referenceSymbol( - plugin.api.getSelector('Injectable'), + plugin.api.selector('Injectable'), ); const symbolClass = plugin.registerSymbol({ exported: true, @@ -138,7 +144,7 @@ const generateAngularClassRequests = ({ }, name: currentClass.className, }), - selector: plugin.api.getSelector('class', currentClass.className), + selector: plugin.api.selector('class', currentClass.className), }); const node = tsc.classDeclaration({ decorator: currentClass.root @@ -166,25 +172,31 @@ const generateAngularFunctionRequests = ({ }: { plugin: AngularCommonPlugin['Instance']; }) => { - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, + operation, + }); - const symbol = plugin.registerSymbol({ - exported: true, - name: plugin.config.httpRequests.methodNameBuilder(operation), - selector: plugin.api.getSelector('httpRequest', operation.id), - }); - const node = generateAngularRequestFunction({ - isRequiredOptions, - operation, - plugin, - symbol, - }); - plugin.setSymbolValue(symbol, node); - }); + const symbol = plugin.registerSymbol({ + exported: true, + name: plugin.config.httpRequests.methodNameBuilder(operation), + selector: plugin.api.selector('httpRequest', operation.id), + }); + const node = generateAngularRequestFunction({ + isRequiredOptions, + operation, + plugin, + symbol, + }); + plugin.setSymbolValue(symbol, node); + }, + { + order: 'declarations', + }, + ); }; const generateRequestCallExpression = ({ @@ -196,10 +208,10 @@ const generateRequestCallExpression = ({ }) => { const client = getClientPlugin(plugin.context.config); const symbolClient = - client.api && 'getSelector' in client.api + client.api && 'selector' in client.api ? plugin.getSymbol( // @ts-expect-error - client.api.getSelector('client'), + client.api.selector('client'), ) : undefined; @@ -265,16 +277,16 @@ const generateAngularRequestMethod = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolHttpRequest = plugin.referenceSymbol( - plugin.api.getSelector('HttpRequest'), + plugin.api.selector('HttpRequest'), ); const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); const dataType = symbolDataType?.placeholder || 'unknown'; @@ -322,16 +334,16 @@ const generateAngularRequestFunction = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolHttpRequest = plugin.referenceSymbol( - plugin.api.getSelector('HttpRequest'), + plugin.api.selector('HttpRequest'), ); const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); const dataType = symbolDataType?.placeholder || 'unknown'; diff --git a/packages/openapi-ts/src/plugins/@angular/common/httpResources.ts b/packages/openapi-ts/src/plugins/@angular/common/httpResources.ts index 4e0c0b0177..2d7fbb818e 100644 --- a/packages/openapi-ts/src/plugins/@angular/common/httpResources.ts +++ b/packages/openapi-ts/src/plugins/@angular/common/httpResources.ts @@ -27,70 +27,76 @@ const generateAngularClassServices = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, + operation, + }); - const classes = operationClasses({ - context: plugin.context, - operation, - plugin: sdkPlugin, - }); + const classes = operationClasses({ + context: plugin.context, + operation, + plugin: sdkPlugin, + }); - for (const entry of classes.values()) { - entry.path.forEach((currentClassName, index) => { - if (!serviceClasses.has(currentClassName)) { - serviceClasses.set(currentClassName, { - className: currentClassName, - classes: new Set(), - methods: new Set(), - nodes: [], - root: !index, - }); - } + for (const entry of classes.values()) { + entry.path.forEach((currentClassName, index) => { + if (!serviceClasses.has(currentClassName)) { + serviceClasses.set(currentClassName, { + className: currentClassName, + classes: new Set(), + methods: new Set(), + nodes: [], + root: !index, + }); + } - const parentClassName = entry.path[index - 1]; - if (parentClassName && parentClassName !== currentClassName) { - const parentClass = serviceClasses.get(parentClassName)!; - parentClass.classes.add(currentClassName); - serviceClasses.set(parentClassName, parentClass); - } + const parentClassName = entry.path[index - 1]; + if (parentClassName && parentClassName !== currentClassName) { + const parentClass = serviceClasses.get(parentClassName)!; + parentClass.classes.add(currentClassName); + serviceClasses.set(parentClassName, parentClass); + } - const isLast = entry.path.length === index + 1; - if (!isLast) { - return; - } + const isLast = entry.path.length === index + 1; + if (!isLast) { + return; + } - const currentClass = serviceClasses.get(currentClassName)!; + const currentClass = serviceClasses.get(currentClassName)!; - const resourceMethodName = - plugin.config.httpResources.methodNameBuilder(operation); + const resourceMethodName = + plugin.config.httpResources.methodNameBuilder(operation); - if (currentClass.methods.has(resourceMethodName)) { - return; - } + if (currentClass.methods.has(resourceMethodName)) { + return; + } - const methodNode = generateAngularResourceMethod({ - isRequiredOptions, - methodName: resourceMethodName, - operation, - plugin, - }); + const methodNode = generateAngularResourceMethod({ + isRequiredOptions, + methodName: resourceMethodName, + operation, + plugin, + }); - if (!currentClass.nodes.length) { - currentClass.nodes.push(methodNode); - } else { - // @ts-expect-error - currentClass.nodes.push(tsc.identifier({ text: '\n' }), methodNode); - } + if (!currentClass.nodes.length) { + currentClass.nodes.push(methodNode); + } else { + // @ts-expect-error + currentClass.nodes.push(tsc.identifier({ text: '\n' }), methodNode); + } - currentClass.methods.add(resourceMethodName); - serviceClasses.set(currentClassName, currentClass); - }); - } - }); + currentClass.methods.add(resourceMethodName); + serviceClasses.set(currentClassName, currentClass); + }); + } + }, + { + order: 'declarations', + }, + ); const generateClass = (currentClass: AngularServiceClassEntry) => { if (generatedClasses.has(currentClass.className)) { @@ -126,7 +132,7 @@ const generateAngularClassServices = ({ } const symbolInjectable = plugin.referenceSymbol( - plugin.api.getSelector('Injectable'), + plugin.api.selector('Injectable'), ); const symbolClass = plugin.registerSymbol({ exported: true, @@ -164,24 +170,30 @@ const generateAngularFunctionServices = ({ }: { plugin: AngularCommonPlugin['Instance']; }) => { - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, + operation, + }); - const symbol = plugin.registerSymbol({ - exported: true, - name: plugin.config.httpResources.methodNameBuilder(operation), - }); - const node = generateAngularResourceFunction({ - isRequiredOptions, - operation, - plugin, - symbol, - }); - plugin.setSymbolValue(symbol, node); - }); + const symbol = plugin.registerSymbol({ + exported: true, + name: plugin.config.httpResources.methodNameBuilder(operation), + }); + const node = generateAngularResourceFunction({ + isRequiredOptions, + operation, + plugin, + symbol, + }); + plugin.setSymbolValue(symbol, node); + }, + { + order: 'declarations', + }, + ); }; const generateResourceCallExpression = ({ @@ -195,11 +207,11 @@ const generateResourceCallExpression = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolHttpResource = plugin.referenceSymbol( - plugin.api.getSelector('httpResource'), + plugin.api.selector('httpResource'), ); const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), + pluginTypeScript.api.selector('response', operation.id), ); const responseType = symbolResponseType?.placeholder || 'unknown'; @@ -216,12 +228,12 @@ const generateResourceCallExpression = ({ // Import the root class from HTTP requests const rootClassName = firstEntry.path[0]!; const symbolClass = plugin.referenceSymbol( - plugin.api.getSelector('class', rootClassName), + plugin.api.selector('class', rootClassName), ); // Build the method access path using inject const symbolInject = plugin.referenceSymbol( - plugin.api.getSelector('inject'), + plugin.api.selector('inject'), ); let methodAccess: ts.Expression = tsc.callExpression({ functionName: symbolInject.placeholder, @@ -282,7 +294,7 @@ const generateResourceCallExpression = ({ } } else { const symbolHttpRequest = plugin.referenceSymbol( - plugin.api.getSelector('httpRequest', operation.id), + plugin.api.selector('httpRequest', operation.id), ); return tsc.callExpression({ @@ -351,11 +363,11 @@ const generateAngularResourceMethod = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); const dataType = symbolDataType?.placeholder || 'unknown'; @@ -404,11 +416,11 @@ const generateAngularResourceFunction = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); const dataType = symbolDataType?.placeholder || 'unknown'; diff --git a/packages/openapi-ts/src/plugins/@angular/common/plugin.ts b/packages/openapi-ts/src/plugins/@angular/common/plugin.ts index 289723764d..da20d8c6a9 100644 --- a/packages/openapi-ts/src/plugins/@angular/common/plugin.ts +++ b/packages/openapi-ts/src/plugins/@angular/common/plugin.ts @@ -9,22 +9,22 @@ export const handler: AngularCommonPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'HttpRequest', - selector: plugin.api.getSelector('HttpRequest'), + selector: plugin.api.selector('HttpRequest'), }); plugin.registerSymbol({ external: '@angular/core', name: 'inject', - selector: plugin.api.getSelector('inject'), + selector: plugin.api.selector('inject'), }); plugin.registerSymbol({ external: '@angular/core', name: 'Injectable', - selector: plugin.api.getSelector('Injectable'), + selector: plugin.api.selector('Injectable'), }); plugin.registerSymbol({ external: '@angular/common/http', name: 'httpResource', - selector: plugin.api.getSelector('httpResource'), + selector: plugin.api.selector('httpResource'), }); if (plugin.config.httpRequests.enabled) { diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-angular/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-angular/api.ts index 5af4687d0f..421e8e03e0 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-angular/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-angular/api.ts @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-angular'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-axios/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-axios/api.ts index 9414a28fe2..08ee36a65e 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-axios/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-axios/api.ts @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-axios'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts b/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts index 35c4ee0e9d..b7180fc353 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts @@ -38,7 +38,7 @@ export const createClient: PluginHandler = ({ plugin }) => { }); const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolClientOptions = plugin.referenceSymbol( - pluginTypeScript.api.getSelector('ClientOptions'), + pluginTypeScript.api.selector('ClientOptions'), ); const { runtimeConfigPath } = plugin.config; @@ -93,7 +93,7 @@ export const createClient: PluginHandler = ({ plugin }) => { const symbolClient = plugin.registerSymbol({ name: 'client', - selector: plugin.api.getSelector('client'), + selector: plugin.api.selector('client'), }); const statement = tsc.constVariable({ exportConst: true, diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts b/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts index 972f9b6964..cb39f82ff6 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts @@ -8,7 +8,7 @@ export const createClientConfigType = ({ const clientModule = clientFolderAbsolutePath(plugin.context.config); const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolClientOptions = plugin.referenceSymbol( - pluginTypeScript.api.getSelector('ClientOptions'), + pluginTypeScript.api.selector('ClientOptions'), ); const symbolConfig = plugin.registerSymbol({ external: clientModule, diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-fetch/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-fetch/api.ts index 1a4e9f7420..d721530649 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-fetch/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-fetch/api.ts @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-fetch'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-next/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-next/api.ts index 0aa1c5e88d..6db16fbf8a 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-next/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-next/api.ts @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-next'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/api.ts index ddb217aa18..b5965c69b1 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/api.ts @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-nuxt'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/api.ts index 11a2383879..479523b23e 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/api.ts @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-ofetch'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts index a7e8276307..0739161609 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts @@ -88,7 +88,7 @@ describe('generateLegacySchemas', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -100,7 +100,7 @@ describe('generateLegacySchemas', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -111,8 +111,8 @@ describe('generateLegacySchemas', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', @@ -251,7 +251,7 @@ describe('generateLegacySchemas', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -264,7 +264,7 @@ describe('generateLegacySchemas', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -275,8 +275,8 @@ describe('generateLegacySchemas', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/api.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/api.ts index 0deea2b62e..71d0200edf 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/api.ts @@ -11,13 +11,13 @@ export type IApi = { * - `ref`: `$ref` JSON pointer * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/schemas'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts index c902cbb6de..999f5a693f 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts @@ -370,7 +370,7 @@ const schemasV2_0_X = ({ const symbol = plugin.registerSymbol({ exported: true, name: schemaName({ name, plugin, schema }), - selector: plugin.api.getSelector('ref', name), + selector: plugin.api.selector('ref', name), }); const obj = schemaToJsonSchemaDraft_04({ context, @@ -403,7 +403,7 @@ const schemasV3_0_X = ({ const symbol = plugin.registerSymbol({ exported: true, name: schemaName({ name, plugin, schema }), - selector: plugin.api.getSelector('ref', name), + selector: plugin.api.selector('ref', name), }); const obj = schemaToJsonSchemaDraft_05({ context, @@ -436,7 +436,7 @@ const schemasV3_1_X = ({ const symbol = plugin.registerSymbol({ exported: true, name: schemaName({ name, plugin, schema }), - selector: plugin.api.getSelector('ref', name), + selector: plugin.api.selector('ref', name), }); const obj = schemaToJsonSchema2020_12({ context, diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts index 2c32ca9540..d1e9a4ae55 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts @@ -90,7 +90,7 @@ describe('handlerLegacy', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -102,7 +102,7 @@ describe('handlerLegacy', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { asClass: true, @@ -114,8 +114,8 @@ describe('handlerLegacy', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', @@ -326,7 +326,7 @@ describe('methodNameBuilder', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -338,7 +338,7 @@ describe('methodNameBuilder', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { asClass: true, @@ -350,8 +350,8 @@ describe('methodNameBuilder', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', @@ -484,7 +484,7 @@ describe('methodNameBuilder', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -496,7 +496,7 @@ describe('methodNameBuilder', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { asClass: true, @@ -509,8 +509,8 @@ describe('methodNameBuilder', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', @@ -645,7 +645,7 @@ describe('methodNameBuilder', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -657,7 +657,7 @@ describe('methodNameBuilder', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { asClass: false, @@ -670,8 +670,8 @@ describe('methodNameBuilder', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/api.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/api.ts index 0737a347fa..313c8a5e4e 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/api.ts @@ -30,7 +30,7 @@ export type IApi = { * - `urlSearchParamsBodySerializer`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { @@ -42,7 +42,7 @@ export class Api implements IApi { return createOperationComment(...args); } - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts index 51a7c224cd..2c2bacce3d 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts @@ -160,17 +160,15 @@ export const operationOptionsType = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); const dataType = symbolDataType?.placeholder || 'unknown'; - const symbolOptions = plugin.referenceSymbol( - plugin.api.getSelector('Options'), - ); + const symbolOptions = plugin.referenceSymbol(plugin.api.selector('Options')); if (isNuxtClient) { const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), + pluginTypeScript.api.selector('response', operation.id), ); const responseType = symbolResponseType?.placeholder || 'unknown'; return `${symbolOptions.placeholder}<${nuxtTypeComposable}, ${dataType}, ${responseType}, ${nuxtTypeDefault}>`; @@ -358,7 +356,7 @@ export const operationStatements = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector( + pluginTypeScript.api.selector( isNuxtClient ? 'response' : 'responses', operation.id, ), @@ -366,7 +364,7 @@ export const operationStatements = ({ const responseType = symbolResponseType?.placeholder || 'unknown'; const symbolErrorType = plugin.getSymbol( - pluginTypeScript.api.getSelector( + pluginTypeScript.api.selector( isNuxtClient ? 'error' : 'errors', operation.id, ), @@ -395,7 +393,7 @@ export const operationStatements = ({ switch (operation.body.type) { case 'form-data': { const symbol = plugin.referenceSymbol( - plugin.api.getSelector('formDataBodySerializer'), + plugin.api.selector('formDataBodySerializer'), ); requestOptions.push({ spread: symbol.placeholder }); break; @@ -413,7 +411,7 @@ export const operationStatements = ({ break; case 'url-search-params': { const symbol = plugin.referenceSymbol( - plugin.api.getSelector('urlSearchParamsBodySerializer'), + plugin.api.selector('urlSearchParamsBodySerializer'), ); requestOptions.push({ spread: symbol.placeholder }); break; @@ -468,7 +466,7 @@ export const operationStatements = ({ plugin.config.transformer, ); const symbolResponseTransformer = plugin.getSymbol( - pluginTransformers.api.getSelector('response', operation.id), + pluginTransformers.api.selector('response', operation.id), ); if ( symbolResponseTransformer && @@ -571,7 +569,7 @@ export const operationStatements = ({ config.push(tsc.objectExpression({ obj })); } const symbol = plugin.referenceSymbol( - plugin.api.getSelector('buildClientParams'), + plugin.api.selector('buildClientParams'), ); statements.push( tsc.constVariable({ @@ -626,10 +624,10 @@ export const operationStatements = ({ } const symbolClient = - plugin.config.client && client.api && 'getSelector' in client.api + plugin.config.client && client.api && 'selector' in client.api ? plugin.getSymbol( // @ts-expect-error - client.api.getSelector('client'), + client.api.selector('client'), ) : undefined; 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 f799a89e12..f583ecac71 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts @@ -34,13 +34,13 @@ const createClientClassNodes = ({ }), }); - const symbolClient = plugin.referenceSymbol(plugin.api.getSelector('Client')); + const symbolClient = plugin.referenceSymbol(plugin.api.selector('Client')); const client = getClientPlugin(plugin.context.config); const symClient = - client.api && 'getSelector' in client.api + client.api && 'selector' in client.api ? plugin.getSymbol( // @ts-expect-error - client.api.getSelector('client'), + client.api.selector('client'), ) : undefined; @@ -136,138 +136,144 @@ const generateClassSdk = ({ ? createClientClassNodes({ plugin }) : []; - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); - const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); - const symbolResponse = isNuxtClient - ? plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), - ) - : undefined; - - const classes = operationClasses({ - context: plugin.context, - operation, - plugin, - }); - - for (const entry of classes.values()) { - entry.path.forEach((currentClassName, index) => { - const symbolCurrentClass = plugin.referenceSymbol( - plugin.api.getSelector('class', currentClassName), - ); - if (!sdkClasses.has(symbolCurrentClass.id)) { - sdkClasses.set(symbolCurrentClass.id, { - className: currentClassName, - classes: new Set(), - id: symbolCurrentClass.id, - methods: new Set(), - nodes: [], - root: !index, - }); - } + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, + operation, + }); + const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); + const symbolResponse = isNuxtClient + ? plugin.getSymbol( + pluginTypeScript.api.selector('response', operation.id), + ) + : undefined; + + const classes = operationClasses({ + context: plugin.context, + operation, + plugin, + }); - const parentClassName = entry.path[index - 1]; - if (parentClassName) { - const symbolParentClass = plugin.referenceSymbol( - plugin.api.getSelector('class', parentClassName), + for (const entry of classes.values()) { + entry.path.forEach((currentClassName, index) => { + const symbolCurrentClass = plugin.referenceSymbol( + plugin.api.selector('class', currentClassName), ); - if ( - symbolParentClass.placeholder !== symbolCurrentClass.placeholder - ) { - const parentClass = sdkClasses.get(symbolParentClass.id)!; - parentClass.classes.add(symbolCurrentClass.id); - sdkClasses.set(symbolParentClass.id, parentClass); + if (!sdkClasses.has(symbolCurrentClass.id)) { + sdkClasses.set(symbolCurrentClass.id, { + className: currentClassName, + classes: new Set(), + id: symbolCurrentClass.id, + methods: new Set(), + nodes: [], + root: !index, + }); } - } - const isLast = entry.path.length === index + 1; - // add methods only to the last class - if (!isLast) { - return; - } + const parentClassName = entry.path[index - 1]; + if (parentClassName) { + const symbolParentClass = plugin.referenceSymbol( + plugin.api.selector('class', parentClassName), + ); + if ( + symbolParentClass.placeholder !== symbolCurrentClass.placeholder + ) { + const parentClass = sdkClasses.get(symbolParentClass.id)!; + parentClass.classes.add(symbolCurrentClass.id); + sdkClasses.set(symbolParentClass.id, parentClass); + } + } - const currentClass = sdkClasses.get(symbolCurrentClass.id)!; + const isLast = entry.path.length === index + 1; + // add methods only to the last class + if (!isLast) { + return; + } - // avoid duplicate methods - if (currentClass.methods.has(entry.methodName)) { - return; - } + const currentClass = sdkClasses.get(symbolCurrentClass.id)!; - const opParameters = operationParameters({ - isRequiredOptions, - operation, - plugin, - }); - const statements = operationStatements({ - isRequiredOptions, - opParameters, - operation, - plugin, - }); - const functionNode = tsc.methodDeclaration({ - accessLevel: 'public', - comment: plugin.api.createOperationComment({ operation }), - isStatic: isAngularClient ? false : !plugin.config.instance, - name: entry.methodName, - parameters: opParameters.parameters, - returnType: undefined, - statements, - types: isNuxtClient - ? [ - { - default: tsc.ots.string('$fetch'), - extends: tsc.typeNode( - plugin.referenceSymbol(plugin.api.getSelector('Composable')) - .placeholder, - ), - name: nuxtTypeComposable, - }, - { - default: symbolResponse - ? tsc.typeReferenceNode({ - typeName: symbolResponse.placeholder, - }) - : tsc.typeNode('undefined'), - extends: symbolResponse - ? tsc.typeReferenceNode({ - typeName: symbolResponse.placeholder, - }) - : undefined, - name: nuxtTypeDefault, - }, - ] - : [ - { - default: - ('throwOnError' in client.config - ? client.config.throwOnError - : false) ?? false, - extends: 'boolean', - name: 'ThrowOnError', - }, - ], - }); + // avoid duplicate methods + if (currentClass.methods.has(entry.methodName)) { + return; + } - if (!currentClass.nodes.length) { - currentClass.nodes.push(functionNode); - } else { - currentClass.nodes.push( - // @ts-expect-error - tsc.identifier({ text: '\n' }), - functionNode, - ); - } + const opParameters = operationParameters({ + isRequiredOptions, + operation, + plugin, + }); + const statements = operationStatements({ + isRequiredOptions, + opParameters, + operation, + plugin, + }); + const functionNode = tsc.methodDeclaration({ + accessLevel: 'public', + comment: plugin.api.createOperationComment({ operation }), + isStatic: isAngularClient ? false : !plugin.config.instance, + name: entry.methodName, + parameters: opParameters.parameters, + returnType: undefined, + statements, + types: isNuxtClient + ? [ + { + default: tsc.ots.string('$fetch'), + extends: tsc.typeNode( + plugin.referenceSymbol(plugin.api.selector('Composable')) + .placeholder, + ), + name: nuxtTypeComposable, + }, + { + default: symbolResponse + ? tsc.typeReferenceNode({ + typeName: symbolResponse.placeholder, + }) + : tsc.typeNode('undefined'), + extends: symbolResponse + ? tsc.typeReferenceNode({ + typeName: symbolResponse.placeholder, + }) + : undefined, + name: nuxtTypeDefault, + }, + ] + : [ + { + default: + ('throwOnError' in client.config + ? client.config.throwOnError + : false) ?? false, + extends: 'boolean', + name: 'ThrowOnError', + }, + ], + }); + + if (!currentClass.nodes.length) { + currentClass.nodes.push(functionNode); + } else { + currentClass.nodes.push( + // @ts-expect-error + tsc.identifier({ text: '\n' }), + functionNode, + ); + } - currentClass.methods.add(entry.methodName); + currentClass.methods.add(entry.methodName); - sdkClasses.set(symbolCurrentClass.id, currentClass); - }); - } - }); + sdkClasses.set(symbolCurrentClass.id, currentClass); + }); + } + }, + { + order: 'declarations', + }, + ); const symbolHeyApiClient = plugin.registerSymbol({ exported: false, @@ -324,7 +330,7 @@ const generateClassSdk = ({ const symbol = plugin.registerSymbol({ exported: true, name: currentClass.className, - selector: plugin.api.getSelector('class', currentClass.className), + selector: plugin.api.selector('class', currentClass.className), }); const node = tsc.classDeclaration({ decorator: @@ -335,7 +341,7 @@ const generateClassSdk = ({ providedIn: 'root', }, ], - name: plugin.referenceSymbol(plugin.api.getSelector('Injectable')) + name: plugin.referenceSymbol(plugin.api.selector('Injectable')) .placeholder, } : undefined, @@ -372,83 +378,89 @@ const generateFlatSdk = ({ const client = getClientPlugin(plugin.context.config); const isNuxtClient = client.name === '@hey-api/client-nuxt'; - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); - const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); - const symbolResponse = isNuxtClient - ? plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), - ) - : undefined; - const opParameters = operationParameters({ - isRequiredOptions, - operation, - plugin, - }); - const statements = operationStatements({ - isRequiredOptions, - opParameters, - operation, - plugin, - }); - const symbol = plugin.registerSymbol({ - name: serviceFunctionIdentifier({ - config: plugin.context.config, - handleIllegal: true, - id: operation.id, + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, operation, - }), - selector: plugin.api.getSelector('function', operation.id), - }); - const node = tsc.constVariable({ - comment: plugin.api.createOperationComment({ operation }), - exportConst: true, - expression: tsc.arrowFunction({ - parameters: opParameters.parameters, - returnType: undefined, - statements, - types: isNuxtClient - ? [ - { - default: tsc.ots.string('$fetch'), - extends: tsc.typeNode( - plugin.referenceSymbol(plugin.api.getSelector('Composable')) - .placeholder, - ), - name: nuxtTypeComposable, - }, - { - default: symbolResponse - ? tsc.typeReferenceNode({ - typeName: symbolResponse.placeholder, - }) - : tsc.typeNode('undefined'), - extends: symbolResponse - ? tsc.typeReferenceNode({ - typeName: symbolResponse.placeholder, - }) - : undefined, - name: nuxtTypeDefault, - }, - ] - : [ - { - default: - ('throwOnError' in client.config - ? client.config.throwOnError - : false) ?? false, - extends: 'boolean', - name: 'ThrowOnError', - }, - ], - }), - name: symbol.placeholder, - }); - plugin.setSymbolValue(symbol, node); - }); + }); + const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); + const symbolResponse = isNuxtClient + ? plugin.getSymbol( + pluginTypeScript.api.selector('response', operation.id), + ) + : undefined; + const opParameters = operationParameters({ + isRequiredOptions, + operation, + plugin, + }); + const statements = operationStatements({ + isRequiredOptions, + opParameters, + operation, + plugin, + }); + const symbol = plugin.registerSymbol({ + name: serviceFunctionIdentifier({ + config: plugin.context.config, + handleIllegal: true, + id: operation.id, + operation, + }), + selector: plugin.api.selector('function', operation.id), + }); + const node = tsc.constVariable({ + comment: plugin.api.createOperationComment({ operation }), + exportConst: true, + expression: tsc.arrowFunction({ + parameters: opParameters.parameters, + returnType: undefined, + statements, + types: isNuxtClient + ? [ + { + default: tsc.ots.string('$fetch'), + extends: tsc.typeNode( + plugin.referenceSymbol(plugin.api.selector('Composable')) + .placeholder, + ), + name: nuxtTypeComposable, + }, + { + default: symbolResponse + ? tsc.typeReferenceNode({ + typeName: symbolResponse.placeholder, + }) + : tsc.typeNode('undefined'), + extends: symbolResponse + ? tsc.typeReferenceNode({ + typeName: symbolResponse.placeholder, + }) + : undefined, + name: nuxtTypeDefault, + }, + ] + : [ + { + default: + ('throwOnError' in client.config + ? client.config.throwOnError + : false) ?? false, + extends: 'boolean', + name: 'ThrowOnError', + }, + ], + }), + name: symbol.placeholder, + }); + plugin.setSymbolValue(symbol, node); + }, + { + order: 'declarations', + }, + ); }; export const handler: HeyApiSdkPlugin['Handler'] = ({ plugin }) => { @@ -456,17 +468,17 @@ export const handler: HeyApiSdkPlugin['Handler'] = ({ plugin }) => { plugin.registerSymbol({ external: clientModule, name: 'formDataBodySerializer', - selector: plugin.api.getSelector('formDataBodySerializer'), + selector: plugin.api.selector('formDataBodySerializer'), }); plugin.registerSymbol({ external: clientModule, name: 'urlSearchParamsBodySerializer', - selector: plugin.api.getSelector('urlSearchParamsBodySerializer'), + selector: plugin.api.selector('urlSearchParamsBodySerializer'), }); plugin.registerSymbol({ external: clientModule, name: 'buildClientParams', - selector: plugin.api.getSelector('buildClientParams'), + selector: plugin.api.selector('buildClientParams'), }); const client = getClientPlugin(plugin.context.config); @@ -479,7 +491,7 @@ export const handler: HeyApiSdkPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'Composable', - selector: plugin.api.getSelector('Composable'), + selector: plugin.api.selector('Composable'), }); } @@ -487,7 +499,7 @@ export const handler: HeyApiSdkPlugin['Handler'] = ({ plugin }) => { plugin.registerSymbol({ external: '@angular/core', name: 'Injectable', - selector: plugin.api.getSelector('Injectable'), + selector: plugin.api.selector('Injectable'), }); } diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/typeOptions.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/typeOptions.ts index 15ddccb175..52e5d0bc88 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/typeOptions.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/typeOptions.ts @@ -26,7 +26,7 @@ export const createTypeOptions = ({ kind: 'type', }, name: 'Client', - selector: plugin.api.getSelector('Client'), + selector: plugin.api.selector('Client'), }); const symbolClientOptions = plugin.registerSymbol({ external: clientModule, @@ -41,7 +41,7 @@ export const createTypeOptions = ({ kind: 'type', }, name: 'Options', - selector: plugin.api.getSelector('Options'), + selector: plugin.api.selector('Options'), }); const typeOptions = tsc.typeAliasDeclaration({ @@ -102,7 +102,7 @@ export const createTypeOptions = ({ tsc.typeParameterDeclaration({ constraint: tsc.typeReferenceNode({ typeName: plugin.referenceSymbol( - plugin.api.getSelector('Composable'), + plugin.api.selector('Composable'), ).placeholder, }), defaultType: tsc.typeNode("'$fetch'"), diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/api.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/api.ts index 920c22e804..3a60212bab 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/api.ts @@ -12,13 +12,13 @@ export type IApi = { * - `response-ref`: `$ref` JSON pointer * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/transformers'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } 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 aa203c16f7..485871b921 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts @@ -60,7 +60,7 @@ const processSchemaType = ({ schema: IR.SchemaObject; }): Array => { if (schema.$ref) { - const selector = plugin.api.getSelector('response-ref', schema.$ref); + const selector = plugin.api.selector('response-ref', schema.$ref); if (!plugin.getSymbol(selector)) { const symbol = plugin.registerSymbol({ @@ -322,65 +322,77 @@ const processSchemaType = ({ // handles only response transformers for now export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { - plugin.forEach('operation', ({ operation }) => { - const { response } = operationResponsesMap(operation); - if (!response) return; + plugin.forEach( + 'operation', + ({ operation }) => { + const { response } = operationResponsesMap(operation); + if (!response) return; - if (response.items && response.items.length > 1) { - if (plugin.context.config.logs.level === 'debug') { - console.warn( - `❗️ Transformers warning: route ${createOperationKey(operation)} has ${response.items.length} non-void success responses. This is currently not handled and we will not generate a response transformer. Please open an issue if you'd like this feature https://github.com/hey-api/openapi-ts/issues`, - ); + if (response.items && response.items.length > 1) { + if (plugin.context.config.logs.level === 'debug') { + console.warn( + `❗️ Transformers warning: route ${createOperationKey(operation)} has ${response.items.length} non-void success responses. This is currently not handled and we will not generate a response transformer. Please open an issue if you'd like this feature https://github.com/hey-api/openapi-ts/issues`, + ); + } + return; } - return; - } - const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); - const symbolResponse = plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), - ); - if (!symbolResponse) return; + const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); + const symbolResponse = plugin.getSymbol( + pluginTypeScript.api.selector('response', operation.id), + ); + if (!symbolResponse) return; - const symbolResponseTransformer = plugin.registerSymbol({ - exported: true, - name: buildName({ - config: { - case: 'camelCase', - name: '{{name}}ResponseTransformer', - }, - name: operation.id, - }), - selector: plugin.api.getSelector('response', operation.id), - }); + const symbolResponseTransformer = plugin.registerSymbol({ + exported: true, + name: buildName({ + config: { + case: 'camelCase', + name: '{{name}}ResponseTransformer', + }, + name: operation.id, + }), + selector: plugin.api.selector('response', operation.id), + }); - // TODO: parser - consider handling simple string response which is also a date - const nodes = schemaResponseTransformerNodes({ plugin, schema: response }); - if (nodes.length) { - const responseTransformerNode = tsc.constVariable({ - exportConst: symbolResponseTransformer.exported, - expression: tsc.arrowFunction({ - async: true, - multiLine: true, - parameters: [ - { - name: dataVariableName, - // TODO: parser - add types, generate types without transforms - type: tsc.keywordTypeNode({ keyword: 'any' }), - }, - ], - returnType: tsc.typeReferenceNode({ - typeArguments: [ - tsc.typeReferenceNode({ typeName: symbolResponse.placeholder }), + // TODO: parser - consider handling simple string response which is also a date + const nodes = schemaResponseTransformerNodes({ + plugin, + schema: response, + }); + if (nodes.length) { + const responseTransformerNode = tsc.constVariable({ + exportConst: symbolResponseTransformer.exported, + expression: tsc.arrowFunction({ + async: true, + multiLine: true, + parameters: [ + { + name: dataVariableName, + // TODO: parser - add types, generate types without transforms + type: tsc.keywordTypeNode({ keyword: 'any' }), + }, ], - typeName: 'Promise', + returnType: tsc.typeReferenceNode({ + typeArguments: [ + tsc.typeReferenceNode({ typeName: symbolResponse.placeholder }), + ], + typeName: 'Promise', + }), + statements: ensureStatements(nodes), }), - statements: ensureStatements(nodes), - }), - name: symbolResponseTransformer.placeholder, - }); - plugin.setSymbolValue(symbolResponseTransformer, responseTransformerNode); - } else { - plugin.setSymbolValue(symbolResponseTransformer, null); - } - }); + name: symbolResponseTransformer.placeholder, + }); + plugin.setSymbolValue( + symbolResponseTransformer, + responseTransformerNode, + ); + } else { + plugin.setSymbolValue(symbolResponseTransformer, null); + } + }, + { + order: 'declarations', + }, + ); }; diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts index 2d0e38fc8d..3a59329d11 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts @@ -88,7 +88,7 @@ describe('generateLegacyTypes', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -100,7 +100,7 @@ describe('generateLegacyTypes', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -111,8 +111,8 @@ describe('generateLegacyTypes', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/api.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/api.ts index 9ddaef1719..aacaaf16a9 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/api.ts @@ -18,6 +18,7 @@ type SelectorType = | 'Webhooks'; export type IApi = { + schemaToType: (args: Parameters[0]) => ts.TypeNode; /** * @param type Selector type. * @param value Depends on `type`: @@ -34,14 +35,13 @@ export type IApi = { * - `Webhooks`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; - schemaToType: (args: Parameters[0]) => ts.TypeNode; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/typescript'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/operation.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/operation.ts index b2be1fa9a2..1584241f82 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/operation.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/operation.ts @@ -128,7 +128,7 @@ const operationToDataType = ({ config: plugin.config.requests, name: operation.id, }), - selector: plugin.api.getSelector('data', operation.id), + selector: plugin.api.selector('data', operation.id), }); const type = schemaToType({ plugin, @@ -164,7 +164,7 @@ export const operationToType = ({ config: plugin.config.errors, name: operation.id, }), - selector: plugin.api.getSelector('errors', operation.id), + selector: plugin.api.selector('errors', operation.id), }); const type = schemaToType({ plugin, @@ -190,7 +190,7 @@ export const operationToType = ({ }, name: operation.id, }), - selector: plugin.api.getSelector('error', operation.id), + selector: plugin.api.selector('error', operation.id), }); const type = tsc.indexedAccessTypeNode({ indexType: tsc.typeOperatorNode({ @@ -220,7 +220,7 @@ export const operationToType = ({ config: plugin.config.responses, name: operation.id, }), - selector: plugin.api.getSelector('responses', operation.id), + selector: plugin.api.selector('responses', operation.id), }); const type = schemaToType({ plugin, @@ -246,7 +246,7 @@ export const operationToType = ({ }, name: operation.id, }), - selector: plugin.api.getSelector('response', operation.id), + selector: plugin.api.selector('response', operation.id), }); const type = tsc.indexedAccessTypeNode({ indexType: tsc.typeOperatorNode({ 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 78ac210c8f..0de46c7ba3 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts @@ -7,9 +7,9 @@ import type { Property } from '../../../tsc'; import { tsc } from '../../../tsc'; import { refToName } from '../../../utils/ref'; import { stringCase } from '../../../utils/stringCase'; +import type { SchemaWithType } from '../../shared/types/schema'; import { fieldName } from '../../shared/utils/case'; import { createSchemaComment } from '../../shared/utils/schema'; -import type { SchemaWithType } from '../../zod/shared/types'; import { createClientOptions } from './clientOptions'; import { exportType } from './export'; import { operationToType } from './operation'; @@ -262,9 +262,9 @@ const stringTypeToIdentifier = ({ parts.pop(); // remove the ID part const type = parts.join('_'); - const selector = plugin.api.getSelector('TypeID', type); + const selector = plugin.api.selector('TypeID', type); if (!plugin.getSymbol(selector)) { - const selectorTypeId = plugin.api.getSelector('TypeID'); + const selectorTypeId = plugin.api.selector('TypeID'); if (!plugin.getSymbol(selectorTypeId)) { const symbolTypeId = plugin.registerSymbol({ @@ -447,7 +447,7 @@ export const schemaToType = ({ }): ts.TypeNode => { if (schema.$ref) { const symbol = plugin.referenceSymbol( - plugin.api.getSelector('ref', schema.$ref), + plugin.api.selector('ref', schema.$ref), ); return tsc.typeReferenceNode({ typeName: symbol.placeholder }); } @@ -506,7 +506,7 @@ const handleComponent = ({ config: plugin.config.definitions, name: refToName(id), }), - selector: plugin.api.getSelector('ref', id), + selector: plugin.api.selector('ref', id), }); exportType({ plugin, @@ -529,7 +529,7 @@ export const handler: HeyApiTypeScriptPlugin['Handler'] = ({ plugin }) => { }, name: 'ClientOptions', }), - selector: plugin.api.getSelector('ClientOptions'), + selector: plugin.api.selector('ClientOptions'), }); // reserve identifier for Webhooks const symbolWebhooks = plugin.registerSymbol({ @@ -543,7 +543,7 @@ export const handler: HeyApiTypeScriptPlugin['Handler'] = ({ plugin }) => { }, name: 'Webhooks', }), - selector: plugin.api.getSelector('Webhooks'), + selector: plugin.api.selector('Webhooks'), }); const servers: Array = []; @@ -595,6 +595,9 @@ export const handler: HeyApiTypeScriptPlugin['Handler'] = ({ plugin }) => { break; } }, + { + order: 'declarations', + }, ); createClientOptions({ plugin, servers, symbolClientOptions }); diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/webhook.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/webhook.ts index 003f6238da..7c306aca48 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/webhook.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/webhook.ts @@ -34,7 +34,7 @@ const operationToDataType = ({ }, name: operation.id, }), - selector: plugin.api.getSelector('webhook-payload', operation.id), + selector: plugin.api.selector('webhook-payload', operation.id), }); const type = schemaToType({ plugin, @@ -55,7 +55,7 @@ const operationToDataType = ({ }, name: symbolWebhookPayload.name, placeholder: symbolWebhookPayload.placeholder, - selector: plugin.api.getSelector('ref', symbolWebhookPayload.placeholder), + selector: plugin.api.selector('ref', symbolWebhookPayload.placeholder), }); data.properties.body = { $ref: symbolWebhookPayload.placeholder }; dataRequired.push('body'); @@ -83,7 +83,7 @@ const operationToDataType = ({ config: plugin.config.webhooks, name: operation.id, }), - selector: plugin.api.getSelector('webhook-request', operation.id), + selector: plugin.api.selector('webhook-request', operation.id), }); const type = schemaToType({ plugin, diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/api.ts b/packages/openapi-ts/src/plugins/@pinia/colada/api.ts index 171c8310c7..be883d4c4f 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/api.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/api.ts @@ -28,13 +28,13 @@ export type IApi = { * - `UseQueryOptions`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@pinia/colada'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts b/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts index 59ce43f546..8d587d2fc4 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts @@ -18,7 +18,7 @@ export const createMutationOptions = ({ queryFn: string; }): void => { const symbolMutationOptionsType = plugin.referenceSymbol( - plugin.api.getSelector('UseMutationOptions'), + plugin.api.selector('UseMutationOptions'), ); const typeData = useTypeData({ operation, plugin }); diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/plugin.ts b/packages/openapi-ts/src/plugins/@pinia/colada/plugin.ts index 4de2232854..2adb8654bd 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/plugin.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/plugin.ts @@ -11,7 +11,7 @@ export const handler: PiniaColadaPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'UseMutationOptions', - selector: plugin.api.getSelector('UseMutationOptions'), + selector: plugin.api.selector('UseMutationOptions'), }); plugin.registerSymbol({ external: plugin.name, @@ -19,7 +19,7 @@ export const handler: PiniaColadaPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'UseQueryOptions', - selector: plugin.api.getSelector('UseQueryOptions'), + selector: plugin.api.selector('UseQueryOptions'), }); plugin.registerSymbol({ external: plugin.name, @@ -27,7 +27,7 @@ export const handler: PiniaColadaPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: '_JSONValue', - selector: plugin.api.getSelector('_JSONValue'), + selector: plugin.api.selector('_JSONValue'), }); plugin.registerSymbol({ external: 'axios', @@ -35,52 +35,58 @@ export const handler: PiniaColadaPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'AxiosError', - selector: plugin.api.getSelector('AxiosError'), + selector: plugin.api.selector('AxiosError'), }); const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); - plugin.forEach('operation', ({ operation }) => { - const classes = sdkPlugin.config.asClass - ? operationClasses({ - context: plugin.context, - operation, - plugin: sdkPlugin, - }) - : undefined; - const entry = classes ? classes.values().next().value : undefined; - const queryFn = - // TODO: this should use class graph to determine correct path string - // as it's really easy to break once we change the class casing - entry - ? [ - plugin.referenceSymbol( - sdkPlugin.api.getSelector('class', entry.path[0]), - ).placeholder, - ...entry.path.slice(1).map((className: string) => - stringCase({ - case: 'camelCase', - value: className, - }), - ), - entry.methodName, - ] - .filter(Boolean) - .join('.') - : plugin.referenceSymbol( - sdkPlugin.api.getSelector('function', operation.id), - ).placeholder; + plugin.forEach( + 'operation', + ({ operation }) => { + const classes = sdkPlugin.config.asClass + ? operationClasses({ + context: plugin.context, + operation, + plugin: sdkPlugin, + }) + : undefined; + const entry = classes ? classes.values().next().value : undefined; + const queryFn = + // TODO: this should use class graph to determine correct path string + // as it's really easy to break once we change the class casing + entry + ? [ + plugin.referenceSymbol( + sdkPlugin.api.selector('class', entry.path[0]), + ).placeholder, + ...entry.path.slice(1).map((className: string) => + stringCase({ + case: 'camelCase', + value: className, + }), + ), + entry.methodName, + ] + .filter(Boolean) + .join('.') + : plugin.referenceSymbol( + sdkPlugin.api.selector('function', operation.id), + ).placeholder; - if (plugin.hooks.operation.isQuery(operation)) { - if (plugin.config.queryOptions.enabled) { - createQueryOptions({ operation, plugin, queryFn }); + if (plugin.hooks.operation.isQuery(operation)) { + if (plugin.config.queryOptions.enabled) { + createQueryOptions({ operation, plugin, queryFn }); + } } - } - if (plugin.hooks.operation.isMutation(operation)) { - if (plugin.config.mutationOptions.enabled) { - createMutationOptions({ operation, plugin, queryFn }); + if (plugin.hooks.operation.isMutation(operation)) { + if (plugin.config.mutationOptions.enabled) { + createMutationOptions({ operation, plugin, queryFn }); + } } - } - }); + }, + { + order: 'declarations', + }, + ); }; diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/queryKey.ts b/packages/openapi-ts/src/plugins/@pinia/colada/queryKey.ts index ea619179ed..b53b75f6a0 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/queryKey.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/queryKey.ts @@ -30,13 +30,13 @@ export const createQueryKeyFunction = ({ }, name: 'createQueryKey', }), - selector: plugin.api.getSelector('createQueryKey'), + selector: plugin.api.selector('createQueryKey'), }); const symbolQueryKeyType = plugin.referenceSymbol( - plugin.api.getSelector('QueryKey'), + plugin.api.selector('QueryKey'), ); const symbolJsonValue = plugin.referenceSymbol( - plugin.api.getSelector('_JSONValue'), + plugin.api.selector('_JSONValue'), ); const returnType = tsc.indexedAccessTypeNode({ @@ -53,14 +53,14 @@ export const createQueryKeyFunction = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const client = getClientPlugin(plugin.context.config); const symbolClient = - client.api && 'getSelector' in client.api + client.api && 'selector' in client.api ? plugin.getSymbol( // @ts-expect-error - client.api.getSelector('client'), + client.api.selector('client'), ) : undefined; @@ -289,7 +289,7 @@ const createQueryKeyLiteral = ({ } const symbolCreateQueryKey = plugin.referenceSymbol( - plugin.api.getSelector('createQueryKey'), + plugin.api.selector('createQueryKey'), ); const createQueryKeyCallExpression = tsc.callExpression({ functionName: symbolCreateQueryKey.placeholder, @@ -304,7 +304,7 @@ export const createQueryKeyType = ({ plugin: PiniaColadaPlugin['Instance']; }) => { const symbolJsonValue = plugin.referenceSymbol( - plugin.api.getSelector('_JSONValue'), + plugin.api.selector('_JSONValue'), ); const properties: Array = [ @@ -333,13 +333,13 @@ export const createQueryKeyType = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolQueryKeyType = plugin.registerSymbol({ exported: true, meta: { kind: 'type' }, name: 'QueryKey', - selector: plugin.api.getSelector('QueryKey'), + selector: plugin.api.selector('QueryKey'), }); const queryKeyType = tsc.typeAliasDeclaration({ exportType: symbolQueryKeyType.exported, diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/queryOptions.ts b/packages/openapi-ts/src/plugins/@pinia/colada/queryOptions.ts index 33a696d283..b26ed22c25 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/queryOptions.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/queryOptions.ts @@ -37,7 +37,7 @@ export const createQueryOptions = ({ context: plugin.context, operation, }); - if (!plugin.getSymbol(plugin.api.getSelector('createQueryKey'))) { + if (!plugin.getSymbol(plugin.api.selector('createQueryKey'))) { createQueryKeyType({ plugin }); createQueryKeyFunction({ plugin }); } @@ -63,7 +63,7 @@ export const createQueryOptions = ({ }); } else { const symbolCreateQueryKey = plugin.referenceSymbol( - plugin.api.getSelector('createQueryKey'), + plugin.api.selector('createQueryKey'), ); // Optionally include tags when configured let tagsExpr: ts.Expression | undefined; @@ -155,12 +155,12 @@ export const createQueryOptions = ({ config: plugin.config.queryOptions, name: operation.id, }), - selector: plugin.api.getSelector('queryOptionsFn', operation.id), + selector: plugin.api.selector('queryOptionsFn', operation.id), }); const symbolDefineQueryOptions = plugin.registerSymbol({ external: plugin.name, name: 'defineQueryOptions', - selector: plugin.api.getSelector('defineQueryOptions'), + selector: plugin.api.selector('defineQueryOptions'), }); const statement = tsc.constVariable({ comment: plugin.config.comments diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/useType.ts b/packages/openapi-ts/src/plugins/@pinia/colada/useType.ts index 245051b8af..9bbd473965 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/useType.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/useType.ts @@ -26,7 +26,7 @@ export const useTypeError = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolErrorType = plugin.getSymbol( - pluginTypeScript.api.getSelector('error', operation.id), + pluginTypeScript.api.selector('error', operation.id), ); let typeErrorName: string | undefined = symbolErrorType?.placeholder; @@ -34,7 +34,7 @@ export const useTypeError = ({ typeErrorName = 'Error'; } if (client.name === '@hey-api/client-axios') { - const symbol = plugin.referenceSymbol(plugin.api.getSelector('AxiosError')); + const symbol = plugin.referenceSymbol(plugin.api.selector('AxiosError')); typeErrorName = `${symbol.placeholder}<${typeErrorName}>`; } return typeErrorName; @@ -49,7 +49,7 @@ export const useTypeResponse = ({ }): string => { const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), + pluginTypeScript.api.selector('response', operation.id), ); return symbolResponseType?.placeholder || 'unknown'; }; diff --git a/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/api.ts b/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/api.ts index 9086df45f5..89b5fd940d 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/api.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/api.ts @@ -32,7 +32,7 @@ export type IApi = { * - `useQuery`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { @@ -40,7 +40,7 @@ export class Api implements IApi { public meta: Plugin.Name<'@tanstack/angular-query-experimental'>, ) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts index 66d566b4ab..adabe1c928 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts @@ -27,7 +27,7 @@ const createInfiniteParamsFunction = ({ }, name: 'createInfiniteParams', }), - selector: plugin.api.getSelector('createInfiniteParams'), + selector: plugin.api.selector('createInfiniteParams'), }); const fn = tsc.constVariable({ @@ -238,20 +238,20 @@ export const createInfiniteQueryOptions = ({ operation, }); - if (!plugin.getSymbol(plugin.api.getSelector('createQueryKey'))) { + if (!plugin.getSymbol(plugin.api.selector('createQueryKey'))) { createQueryKeyType({ plugin }); createQueryKeyFunction({ plugin }); } - if (!plugin.getSymbol(plugin.api.getSelector('createInfiniteParams'))) { + if (!plugin.getSymbol(plugin.api.selector('createInfiniteParams'))) { createInfiniteParamsFunction({ plugin }); } const symbolInfiniteQueryOptions = plugin.referenceSymbol( - plugin.api.getSelector('infiniteQueryOptions'), + plugin.api.selector('infiniteQueryOptions'), ); const symbolInfiniteDataType = plugin.referenceSymbol( - plugin.api.getSelector('InfiniteData'), + plugin.api.selector('InfiniteData'), ); const typeData = useTypeData({ operation, plugin }); @@ -259,7 +259,7 @@ export const createInfiniteQueryOptions = ({ const typeResponse = useTypeResponse({ operation, plugin }); const symbolQueryKeyType = plugin.referenceSymbol( - plugin.api.getSelector('QueryKey'), + plugin.api.selector('QueryKey'), ); const typeQueryKey = `${symbolQueryKeyType.placeholder}<${typeData}>`; const typePageObjectParam = `Pick<${typeQueryKey}[0], 'body' | 'headers' | 'path' | 'query'>`; @@ -322,7 +322,7 @@ export const createInfiniteQueryOptions = ({ }); const symbolCreateInfiniteParams = plugin.referenceSymbol( - plugin.api.getSelector('createInfiniteParams'), + plugin.api.selector('createInfiniteParams'), ); const statements: Array = [ diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/mutationOptions.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/mutationOptions.ts index 6084b516ce..26d6f2a027 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/mutationOptions.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/mutationOptions.ts @@ -17,7 +17,7 @@ export const createMutationOptions = ({ queryFn: string; }): void => { const symbolMutationOptionsType = plugin.referenceSymbol( - plugin.api.getSelector('MutationOptions'), + plugin.api.selector('MutationOptions'), ); const typeData = useTypeData({ operation, plugin }); 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 73baee2760..38e10ae6b9 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts @@ -13,7 +13,7 @@ export const handler: PluginHandler = ({ plugin }) => { kind: 'type', }, name: 'DefaultError', - selector: plugin.api.getSelector('DefaultError'), + selector: plugin.api.selector('DefaultError'), }); plugin.registerSymbol({ external: plugin.name, @@ -21,7 +21,7 @@ export const handler: PluginHandler = ({ plugin }) => { kind: 'type', }, name: 'InfiniteData', - selector: plugin.api.getSelector('InfiniteData'), + selector: plugin.api.selector('InfiniteData'), }); const mutationsType = plugin.name === '@tanstack/angular-query-experimental' || @@ -35,22 +35,22 @@ export const handler: PluginHandler = ({ plugin }) => { kind: 'type', }, name: mutationsType, - selector: plugin.api.getSelector('MutationOptions'), + selector: plugin.api.selector('MutationOptions'), }); plugin.registerSymbol({ external: plugin.name, name: 'infiniteQueryOptions', - selector: plugin.api.getSelector('infiniteQueryOptions'), + selector: plugin.api.selector('infiniteQueryOptions'), }); plugin.registerSymbol({ external: plugin.name, name: 'queryOptions', - selector: plugin.api.getSelector('queryOptions'), + selector: plugin.api.selector('queryOptions'), }); plugin.registerSymbol({ external: plugin.name, name: 'useQuery', - selector: plugin.api.getSelector('useQuery'), + selector: plugin.api.selector('useQuery'), }); plugin.registerSymbol({ external: 'axios', @@ -58,60 +58,66 @@ export const handler: PluginHandler = ({ plugin }) => { kind: 'type', }, name: 'AxiosError', - selector: plugin.api.getSelector('AxiosError'), + selector: plugin.api.selector('AxiosError'), }); const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); - plugin.forEach('operation', ({ operation }) => { - const classes = sdkPlugin.config.asClass - ? operationClasses({ - context: plugin.context, - operation, - plugin: sdkPlugin, - }) - : undefined; - const entry = classes ? classes.values().next().value : undefined; - const queryFn = - // TODO: this should use class graph to determine correct path string - // as it's really easy to break once we change the class casing - entry - ? [ - plugin.referenceSymbol( - sdkPlugin.api.getSelector('class', entry.path[0]), - ).placeholder, - ...entry.path.slice(1).map((className) => - stringCase({ - case: 'camelCase', - value: className, - }), - ), - entry.methodName, - ] - .filter(Boolean) - .join('.') - : plugin.referenceSymbol( - sdkPlugin.api.getSelector('function', operation.id), - ).placeholder; + plugin.forEach( + 'operation', + ({ operation }) => { + const classes = sdkPlugin.config.asClass + ? operationClasses({ + context: plugin.context, + operation, + plugin: sdkPlugin, + }) + : undefined; + const entry = classes ? classes.values().next().value : undefined; + const queryFn = + // TODO: this should use class graph to determine correct path string + // as it's really easy to break once we change the class casing + entry + ? [ + plugin.referenceSymbol( + sdkPlugin.api.selector('class', entry.path[0]), + ).placeholder, + ...entry.path.slice(1).map((className) => + stringCase({ + case: 'camelCase', + value: className, + }), + ), + entry.methodName, + ] + .filter(Boolean) + .join('.') + : plugin.referenceSymbol( + sdkPlugin.api.selector('function', operation.id), + ).placeholder; - if (plugin.hooks.operation.isQuery(operation)) { - if (plugin.config.queryOptions.enabled) { - createQueryOptions({ operation, plugin, queryFn }); - } + if (plugin.hooks.operation.isQuery(operation)) { + if (plugin.config.queryOptions.enabled) { + createQueryOptions({ operation, plugin, queryFn }); + } - if (plugin.config.infiniteQueryOptions.enabled) { - createInfiniteQueryOptions({ operation, plugin, queryFn }); - } + if (plugin.config.infiniteQueryOptions.enabled) { + createInfiniteQueryOptions({ operation, plugin, queryFn }); + } - if ('useQuery' in plugin.config && plugin.config.useQuery.enabled) { - createUseQuery({ operation, plugin }); + if ('useQuery' in plugin.config && plugin.config.useQuery.enabled) { + createUseQuery({ operation, plugin }); + } } - } - if (plugin.hooks.operation.isMutation(operation)) { - if (plugin.config.mutationOptions.enabled) { - createMutationOptions({ operation, plugin, queryFn }); + if (plugin.hooks.operation.isMutation(operation)) { + if (plugin.config.mutationOptions.enabled) { + createMutationOptions({ operation, plugin, queryFn }); + } } - } - }); + }, + { + order: 'declarations', + }, + ); }; diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts index 08a80d3705..1adf957479 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts @@ -29,10 +29,10 @@ export const createQueryKeyFunction = ({ }, name: 'createQueryKey', }), - selector: plugin.api.getSelector('createQueryKey'), + selector: plugin.api.selector('createQueryKey'), }); const symbolQueryKeyType = plugin.referenceSymbol( - plugin.api.getSelector('QueryKey'), + plugin.api.selector('QueryKey'), ); const returnType = tsc.indexedAccessTypeNode({ @@ -49,16 +49,16 @@ export const createQueryKeyFunction = ({ const client = getClientPlugin(plugin.context.config); const symbolClient = - client.api && 'getSelector' in client.api + client.api && 'selector' in client.api ? plugin.getSymbol( // @ts-expect-error - client.api.getSelector('client'), + client.api.selector('client'), ) : undefined; const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const fn = tsc.constVariable({ @@ -275,7 +275,7 @@ const createQueryKeyLiteral = ({ } const symbolCreateQueryKey = plugin.referenceSymbol( - plugin.api.getSelector('createQueryKey'), + plugin.api.selector('createQueryKey'), ); const createQueryKeyCallExpression = tsc.callExpression({ functionName: symbolCreateQueryKey.placeholder, @@ -311,7 +311,7 @@ export const createQueryKeyType = ({ plugin }: { plugin: PluginInstance }) => { const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolQueryKeyType = plugin.registerSymbol({ exported: true, @@ -319,7 +319,7 @@ export const createQueryKeyType = ({ plugin }: { plugin: PluginInstance }) => { kind: 'type', }, name: 'QueryKey', - selector: plugin.api.getSelector('QueryKey'), + selector: plugin.api.selector('QueryKey'), }); const queryKeyType = tsc.typeAliasDeclaration({ exportType: symbolQueryKeyType.exported, diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryOptions.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryOptions.ts index de68661af6..25b52f9e4c 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryOptions.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryOptions.ts @@ -36,13 +36,13 @@ export const createQueryOptions = ({ operation, }); - if (!plugin.getSymbol(plugin.api.getSelector('createQueryKey'))) { + if (!plugin.getSymbol(plugin.api.selector('createQueryKey'))) { createQueryKeyType({ plugin }); createQueryKeyFunction({ plugin }); } const symbolQueryOptions = plugin.referenceSymbol( - plugin.api.getSelector('queryOptions'), + plugin.api.selector('queryOptions'), ); const symbolQueryKey = plugin.registerSymbol({ @@ -160,7 +160,7 @@ export const createQueryOptions = ({ config: plugin.config.queryOptions, name: operation.id, }), - selector: plugin.api.getSelector('queryOptionsFn', operation.id), + selector: plugin.api.selector('queryOptionsFn', operation.id), }); const statement = tsc.constVariable({ comment: plugin.config.comments diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/useQuery.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/useQuery.ts index 085e72052c..03c88edcab 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/useQuery.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/useQuery.ts @@ -36,7 +36,7 @@ export const createUseQuery = ({ }); const symbolUseQuery = plugin.referenceSymbol( - plugin.api.getSelector('useQuery'), + plugin.api.selector('useQuery'), ); const isRequiredOptions = isOperationOptionsRequired({ @@ -46,7 +46,7 @@ export const createUseQuery = ({ const typeData = useTypeData({ operation, plugin }); const symbolQueryOptionsFn = plugin.referenceSymbol( - plugin.api.getSelector('queryOptionsFn', operation.id), + plugin.api.selector('queryOptionsFn', operation.id), ); const statement = tsc.constVariable({ comment: plugin.config.comments diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/useType.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/useType.ts index 41f0b66243..18e85cef0d 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/useType.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/useType.ts @@ -26,18 +26,16 @@ export const useTypeError = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolErrorType = plugin.getSymbol( - pluginTypeScript.api.getSelector('error', operation.id), + pluginTypeScript.api.selector('error', operation.id), ); let typeErrorName: string | undefined = symbolErrorType?.placeholder; if (!typeErrorName) { - const symbol = plugin.referenceSymbol( - plugin.api.getSelector('DefaultError'), - ); + const symbol = plugin.referenceSymbol(plugin.api.selector('DefaultError')); typeErrorName = symbol.placeholder; } if (client.name === '@hey-api/client-axios') { - const symbol = plugin.referenceSymbol(plugin.api.getSelector('AxiosError')); + const symbol = plugin.referenceSymbol(plugin.api.selector('AxiosError')); typeErrorName = `${symbol.placeholder}<${typeErrorName}>`; } return typeErrorName; @@ -52,7 +50,7 @@ export const useTypeResponse = ({ }): string => { const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), + pluginTypeScript.api.selector('response', operation.id), ); return symbolResponseType?.placeholder || 'unknown'; }; diff --git a/packages/openapi-ts/src/plugins/@tanstack/react-query/api.ts b/packages/openapi-ts/src/plugins/@tanstack/react-query/api.ts index 33357cb955..1023a72a14 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/react-query/api.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/react-query/api.ts @@ -32,13 +32,13 @@ export type IApi = { * - `useQuery`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@tanstack/react-query'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@tanstack/solid-query/api.ts b/packages/openapi-ts/src/plugins/@tanstack/solid-query/api.ts index b21c391db4..b6f51efa13 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/solid-query/api.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/solid-query/api.ts @@ -32,13 +32,13 @@ export type IApi = { * - `useQuery`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@tanstack/solid-query'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@tanstack/svelte-query/api.ts b/packages/openapi-ts/src/plugins/@tanstack/svelte-query/api.ts index db6b292e6f..50880f8d9b 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/svelte-query/api.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/svelte-query/api.ts @@ -32,13 +32,13 @@ export type IApi = { * - `useQuery`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@tanstack/svelte-query'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@tanstack/vue-query/api.ts b/packages/openapi-ts/src/plugins/@tanstack/vue-query/api.ts index 72a6c77b09..334f0efede 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/vue-query/api.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/vue-query/api.ts @@ -32,13 +32,13 @@ export type IApi = { * - `useQuery`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@tanstack/vue-query'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/arktype/api.ts b/packages/openapi-ts/src/plugins/arktype/api.ts new file mode 100644 index 0000000000..610106a3b3 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/api.ts @@ -0,0 +1,55 @@ +import type { Selector } from '@hey-api/codegen-core'; +import type ts from 'typescript'; + +import type { Plugin } from '../types'; +import type { ValidatorArgs } from './shared/types'; +import { createRequestValidatorV2, createResponseValidatorV2 } from './v2/api'; + +type SelectorType = + | 'data' + | 'external' + | 'ref' + | 'responses' + | 'type-infer-data' + | 'type-infer-ref' + | 'type-infer-responses' + | 'type-infer-webhook-request' + | 'webhook-request'; + +export type IApi = { + createRequestValidator: (args: ValidatorArgs) => ts.ArrowFunction | undefined; + createResponseValidator: ( + args: ValidatorArgs, + ) => ts.ArrowFunction | undefined; + /** + * @param type Selector type. + * @param value Depends on `type`: + * - `data`: `operation.id` string + * - `external`: external modules + * - `ref`: `$ref` JSON pointer + * - `responses`: `operation.id` string + * - `type-infer-data`: `operation.id` string + * - `type-infer-ref`: `$ref` JSON pointer + * - `type-infer-responses`: `operation.id` string + * - `type-infer-webhook-request`: `operation.id` string + * - `webhook-request`: `operation.id` string + * @returns Selector array + */ + selector: (type: SelectorType, value?: string) => Selector; +}; + +export class Api implements IApi { + constructor(public meta: Plugin.Name<'arktype'>) {} + + createRequestValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + return createRequestValidatorV2(args); + } + + createResponseValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + return createResponseValidatorV2(args); + } + + selector(...args: ReadonlyArray): Selector { + return [this.meta.name, ...(args as Selector)]; + } +} diff --git a/packages/openapi-ts/src/plugins/arktype/config.ts b/packages/openapi-ts/src/plugins/arktype/config.ts new file mode 100644 index 0000000000..60bde7267f --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/config.ts @@ -0,0 +1,271 @@ +import { definePluginConfig, mappers } from '../shared/utils/config'; +import { Api } from './api'; +import { handler } from './plugin'; +import type { ArktypePlugin } from './types'; + +export const defaultConfig: ArktypePlugin['Config'] = { + api: new Api({ + name: 'arktype', + }), + config: { + case: 'PascalCase', + comments: true, + exportFromIndex: false, + metadata: false, + }, + handler, + name: 'arktype', + resolveConfig: (plugin, context) => { + plugin.config.types = context.valueToObject({ + defaultValue: { + infer: { + case: 'PascalCase', + enabled: false, + }, + }, + mappers: { + object: (fields, defaultValue) => ({ + ...fields, + infer: context.valueToObject({ + defaultValue: { + ...(defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + >), + enabled: + fields.infer !== undefined + ? Boolean(fields.infer) + : ( + defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + > + ).enabled, + }, + mappers, + value: fields.infer, + }), + }), + }, + value: plugin.config.types, + }); + + plugin.config.definitions = context.valueToObject({ + defaultValue: { + case: plugin.config.case ?? 'PascalCase', + enabled: true, + name: '{{name}}', + types: { + ...plugin.config.types, + infer: { + ...(plugin.config.types.infer as Extract< + typeof plugin.config.types.infer, + Record + >), + name: '{{name}}', + }, + }, + }, + mappers: { + ...mappers, + object: (fields, defaultValue) => ({ + ...fields, + types: context.valueToObject({ + defaultValue: defaultValue.types!, + mappers: { + object: (fields, defaultValue) => ({ + ...fields, + infer: context.valueToObject({ + defaultValue: { + ...(defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + >), + enabled: + fields.infer !== undefined + ? Boolean(fields.infer) + : ( + defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + > + ).enabled, + }, + mappers, + value: fields.infer, + }), + }), + }, + value: fields.types, + }), + }), + }, + value: plugin.config.definitions, + }); + + plugin.config.requests = context.valueToObject({ + defaultValue: { + case: plugin.config.case ?? 'PascalCase', + enabled: true, + name: '{{name}}Data', + types: { + ...plugin.config.types, + infer: { + ...(plugin.config.types.infer as Extract< + typeof plugin.config.types.infer, + Record + >), + name: '{{name}}Data', + }, + }, + }, + mappers: { + ...mappers, + object: (fields, defaultValue) => ({ + ...fields, + types: context.valueToObject({ + defaultValue: defaultValue.types!, + mappers: { + object: (fields, defaultValue) => ({ + ...fields, + infer: context.valueToObject({ + defaultValue: { + ...(defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + >), + enabled: + fields.infer !== undefined + ? Boolean(fields.infer) + : ( + defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + > + ).enabled, + }, + mappers, + value: fields.infer, + }), + }), + }, + value: fields.types, + }), + }), + }, + value: plugin.config.requests, + }); + + plugin.config.responses = context.valueToObject({ + defaultValue: { + case: plugin.config.case ?? 'PascalCase', + enabled: true, + name: '{{name}}Response', + types: { + ...plugin.config.types, + infer: { + ...(plugin.config.types.infer as Extract< + typeof plugin.config.types.infer, + Record + >), + name: '{{name}}Response', + }, + }, + }, + mappers: { + ...mappers, + object: (fields, defaultValue) => ({ + ...fields, + types: context.valueToObject({ + defaultValue: defaultValue.types!, + mappers: { + object: (fields, defaultValue) => ({ + ...fields, + infer: context.valueToObject({ + defaultValue: { + ...(defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + >), + enabled: + fields.infer !== undefined + ? Boolean(fields.infer) + : ( + defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + > + ).enabled, + }, + mappers, + value: fields.infer, + }), + }), + }, + value: fields.types, + }), + }), + }, + value: plugin.config.responses, + }); + + plugin.config.webhooks = context.valueToObject({ + defaultValue: { + case: plugin.config.case ?? 'PascalCase', + enabled: true, + name: '{{name}}WebhookRequest', + types: { + ...plugin.config.types, + infer: { + ...(plugin.config.types.infer as Extract< + typeof plugin.config.types.infer, + Record + >), + name: '{{name}}WebhookRequest', + }, + }, + }, + mappers: { + ...mappers, + object: (fields, defaultValue) => ({ + ...fields, + types: context.valueToObject({ + defaultValue: defaultValue.types!, + mappers: { + object: (fields, defaultValue) => ({ + ...fields, + infer: context.valueToObject({ + defaultValue: { + ...(defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + >), + enabled: + fields.infer !== undefined + ? Boolean(fields.infer) + : ( + defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + > + ).enabled, + }, + mappers, + value: fields.infer, + }), + }), + }, + value: fields.types, + }), + }), + }, + value: plugin.config.webhooks, + }); + }, + tags: ['validator'], +}; + +/** + * Type helper for Arktype plugin, returns {@link Plugin.Config} object + */ +export const defineConfig = definePluginConfig(defaultConfig); diff --git a/packages/openapi-ts/src/plugins/arktype/constants.ts b/packages/openapi-ts/src/plugins/arktype/constants.ts new file mode 100644 index 0000000000..7744ee056b --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/constants.ts @@ -0,0 +1,115 @@ +import { tsc } from '../../tsc'; + +export const identifiers = { + keywords: { + false: tsc.identifier({ text: 'false' }), + true: tsc.identifier({ text: 'true' }), + }, + /** + * {@link https://arktype.io/docs/primitives#number Number} + */ + number: { + Infinity: tsc.identifier({ text: 'Infinity' }), + NaN: tsc.identifier({ text: 'NaN' }), + NegativeInfinity: tsc.identifier({ text: 'NegativeInfinity' }), + epoch: tsc.identifier({ text: 'epoch' }), + integer: tsc.identifier({ text: 'integer' }), + safe: tsc.identifier({ text: 'safe' }), + }, + /** + * {@link https://arktype.io/docs/primitives Primitives} + */ + primitives: { + bigint: tsc.identifier({ text: 'bigint' }), + boolean: tsc.identifier({ text: 'boolean' }), + keywords: tsc.identifier({ text: 'keywords' }), + null: 'null', + number: tsc.identifier({ text: 'number' }), + string: 'string', + symbol: tsc.identifier({ text: 'symbol' }), + undefined: tsc.identifier({ text: 'undefined' }), + unit: tsc.identifier({ text: 'unit' }), + }, + /** + * {@link https://arktype.io/docs/primitives#string String} + */ + string: { + NFC: tsc.identifier({ text: 'NFC' }), + NFD: tsc.identifier({ text: 'NFD' }), + NFKC: tsc.identifier({ text: 'NFKC' }), + NFKD: tsc.identifier({ text: 'NFKD' }), + alpha: tsc.identifier({ text: 'alpha' }), + alphanumeric: tsc.identifier({ text: 'alphanumeric' }), + base64: tsc.identifier({ text: 'base64' }), + capitalize: tsc.identifier({ text: 'capitalize' }), + creditCard: tsc.identifier({ text: 'creditCard' }), + date: 'date', + digits: tsc.identifier({ text: 'digits' }), + email: 'email', + epoch: tsc.identifier({ text: 'epoch' }), + hex: tsc.identifier({ text: 'hex' }), + integer: tsc.identifier({ text: 'integer' }), + ip: 'ip', + iso: 'iso', + json: tsc.identifier({ text: 'json' }), + lower: tsc.identifier({ text: 'lower' }), + normalize: tsc.identifier({ text: 'normalize' }), + numeric: tsc.identifier({ text: 'numeric' }), + parse: tsc.identifier({ text: 'parse' }), + preformatted: tsc.identifier({ text: 'preformatted' }), + regex: tsc.identifier({ text: 'regex' }), + semver: tsc.identifier({ text: 'semver' }), + trim: tsc.identifier({ text: 'trim' }), + upper: tsc.identifier({ text: 'upper' }), + url: 'url', + uuid: 'uuid', + v1: 'v1', + v2: 'v2', + v3: 'v3', + v4: 'v4', + v5: 'v5', + v6: 'v6', + v7: 'v7', + v8: 'v8', + }, + /** + * {@link https://arktype.io/docs/type-api Type API} + */ + type: { + $: tsc.identifier({ text: '$' }), + allows: tsc.identifier({ text: 'allows' }), + and: tsc.identifier({ text: 'and' }), + array: tsc.identifier({ text: 'array' }), + as: tsc.identifier({ text: 'as' }), + assert: tsc.identifier({ text: 'assert' }), + brand: tsc.identifier({ text: 'brand' }), + configure: tsc.identifier({ text: 'configure' }), + default: tsc.identifier({ text: 'default' }), + describe: tsc.identifier({ text: 'describe' }), + description: tsc.identifier({ text: 'description' }), + equals: tsc.identifier({ text: 'equals' }), + exclude: tsc.identifier({ text: 'exclude' }), + expression: tsc.identifier({ text: 'expression' }), + extends: tsc.identifier({ text: 'extends' }), + extract: tsc.identifier({ text: 'extract' }), + filter: tsc.identifier({ text: 'filter' }), + from: tsc.identifier({ text: 'from' }), + ifEquals: tsc.identifier({ text: 'ifEquals' }), + ifExtends: tsc.identifier({ text: 'ifExtends' }), + infer: tsc.identifier({ text: 'infer' }), + inferIn: tsc.identifier({ text: 'inferIn' }), + intersect: tsc.identifier({ text: 'intersect' }), + json: tsc.identifier({ text: 'json' }), + meta: tsc.identifier({ text: 'meta' }), + narrow: tsc.identifier({ text: 'narrow' }), + onDeepUndeclaredKey: tsc.identifier({ text: 'onDeepUndeclaredKey' }), + onUndeclaredKey: tsc.identifier({ text: 'onUndeclaredKey' }), + optional: tsc.identifier({ text: 'optional' }), + or: tsc.identifier({ text: 'or' }), + overlaps: tsc.identifier({ text: 'overlaps' }), + pipe: tsc.identifier({ text: 'pipe' }), + select: tsc.identifier({ text: 'select' }), + to: tsc.identifier({ text: 'to' }), + toJsonSchema: tsc.identifier({ text: 'toJsonSchema' }), + }, +}; diff --git a/packages/openapi-ts/src/plugins/arktype/index.ts b/packages/openapi-ts/src/plugins/arktype/index.ts new file mode 100644 index 0000000000..3325c55531 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/index.ts @@ -0,0 +1,2 @@ +export { defaultConfig, defineConfig } from './config'; +export type { ArktypePlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/arktype/plugin.ts b/packages/openapi-ts/src/plugins/arktype/plugin.ts new file mode 100644 index 0000000000..205edde7c6 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/plugin.ts @@ -0,0 +1,4 @@ +import type { ArktypePlugin } from './types'; +import { handlerV2 } from './v2/plugin'; + +export const handler: ArktypePlugin['Handler'] = (args) => handlerV2(args); diff --git a/packages/openapi-ts/src/plugins/arktype/shared/export.ts b/packages/openapi-ts/src/plugins/arktype/shared/export.ts new file mode 100644 index 0000000000..c08731bd8a --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/shared/export.ts @@ -0,0 +1,62 @@ +import type { Symbol } from '@hey-api/codegen-core'; +import ts from 'typescript'; + +import type { IR } from '../../../ir/types'; +import { tsc } from '../../../tsc'; +import { createSchemaComment } from '../../shared/utils/schema'; +import { identifiers } from '../constants'; +import type { ArktypePlugin } from '../types'; +import type { Ast } from './types'; + +export const exportAst = ({ + ast, + plugin, + schema, + symbol, + typeInferSymbol, +}: { + ast: Ast; + plugin: ArktypePlugin['Instance']; + schema: IR.SchemaObject; + symbol: Symbol; + typeInferSymbol: Symbol | undefined; +}): void => { + const type = plugin.referenceSymbol( + plugin.api.selector('external', 'arktype.type'), + ); + + const statement = tsc.constVariable({ + comment: plugin.config.comments + ? createSchemaComment({ schema }) + : undefined, + exportConst: symbol.exported, + expression: tsc.callExpression({ + functionName: type.placeholder, + parameters: [ + ast.def ? tsc.stringLiteral({ text: ast.def }) : ast.expression, + ], + }), + name: symbol.placeholder, + // typeName: ast.typeName + // ? (tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: ast.typeName, + // }) as unknown as ts.TypeNode) + // : undefined, + }); + plugin.setSymbolValue(symbol, statement); + + if (typeInferSymbol) { + const inferType = tsc.typeAliasDeclaration({ + exportType: typeInferSymbol.exported, + name: typeInferSymbol.placeholder, + type: ts.factory.createTypeQueryNode( + ts.factory.createQualifiedName( + ts.factory.createIdentifier(symbol.placeholder), + identifiers.type.infer, + ), + ), + }); + plugin.setSymbolValue(typeInferSymbol, inferType); + } +}; diff --git a/packages/openapi-ts/src/plugins/arktype/shared/types.d.ts b/packages/openapi-ts/src/plugins/arktype/shared/types.d.ts new file mode 100644 index 0000000000..810a96dad0 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/shared/types.d.ts @@ -0,0 +1,30 @@ +import type ts from 'typescript'; + +import type { IR } from '../../../ir/types'; +import type { ToRefs } from '../../shared/types/refs'; +import type { ArktypePlugin } from '../types'; + +export type Ast = { + def: string; + expression: ts.Expression; + hasLazyExpression?: boolean; + typeName?: string | ts.Identifier; +}; + +export type IrSchemaToAstOptions = { + plugin: ArktypePlugin['Instance']; + state: ToRefs; +}; + +export type PluginState = { + /** + * Path to the schema in the intermediary representation. + */ + _path: ReadonlyArray; + hasLazyExpression: boolean; +}; + +export type ValidatorArgs = { + operation: IR.OperationObject; + plugin: ArktypePlugin['Instance']; +}; diff --git a/packages/openapi-ts/src/plugins/arktype/types.d.ts b/packages/openapi-ts/src/plugins/arktype/types.d.ts new file mode 100644 index 0000000000..e7f940787e --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/types.d.ts @@ -0,0 +1,665 @@ +import type { StringCase, StringName } from '../../types/case'; +import type { DefinePlugin, Plugin } from '../types'; +import type { IApi } from './api'; + +export type UserConfig = Plugin.Name<'arktype'> & + Plugin.Hooks & { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Add comments from input to the generated Arktype schemas? + * + * @default true + */ + comments?: boolean; + /** + * Configuration for reusable schema definitions. + * + * Controls generation of shared Arktype schemas that can be referenced + * across requests and responses. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + */ + definitions?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate Arktype schemas for reusable definitions. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated schema names. The name variable + * is obtained from the schema name. + * + * @default '{{name}}' + */ + name?: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types?: { + /** + * Configuration for `infer` types. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default false + */ + infer?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}' + */ + name?: StringName; + }; + }; + }; + /** + * Should the exports from the generated files be re-exported in the index + * barrel file? + * + * @default false + */ + exportFromIndex?: boolean; + /** + * Enable Arktype metadata support? It's often useful to associate a schema + * with some additional metadata for documentation, code generation, AI + * structured outputs, form validation, and other purposes. + * + * @default false + */ + metadata?: boolean; + /** + * Configuration for request-specific Arktype schemas. + * + * Controls generation of Arktype schemas for request bodies, query parameters, path + * parameters, and headers. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default true + */ + requests?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate Arktype schemas for request definitions. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated schema names. The name variable + * is obtained from the operation name. + * + * @default '{{name}}Data' + */ + name?: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types?: { + /** + * Configuration for `infer` types. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default false + */ + infer?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}Data' + */ + name?: StringName; + }; + }; + }; + /** + * Configuration for response-specific Arktype schemas. + * + * Controls generation of Arktype schemas for response bodies, error responses, + * and status codes. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default true + */ + responses?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate Arktype schemas for response definitions. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated schema names. The name variable + * is obtained from the operation name. + * + * @default '{{name}}Response' + */ + name?: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types?: { + /** + * Configuration for `infer` types. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default false + */ + infer?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}Response' + */ + name?: StringName; + }; + }; + }; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types?: { + /** + * Configuration for `infer` types. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default false + */ + infer?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled?: boolean; + }; + }; + /** + * Configuration for webhook-specific Arktype schemas. + * + * Controls generation of Arktype schemas for webhook payloads. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default true + */ + webhooks?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate Arktype schemas for webhook definitions. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated schema names. The name variable + * is obtained from the webhook key. + * + * @default '{{name}}WebhookRequest' + */ + name?: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types?: { + /** + * Configuration for `infer` types. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default false + */ + infer?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}WebhookRequest' + */ + name?: StringName; + }; + }; + }; + }; + +export type Config = Plugin.Name<'arktype'> & + Plugin.Hooks & { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Add comments from input to the generated Arktype schemas? + * + * @default true + */ + comments: boolean; + /** + * Configuration for reusable schema definitions. + * + * Controls generation of shared Arktype schemas that can be referenced across + * requests and responses. + */ + definitions: { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate Arktype schemas for reusable definitions. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated schema names. The name variable is + * obtained from the schema name. + * + * @default '{{name}}' + */ + name: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types: { + /** + * Configuration for `infer` types. + */ + infer: { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}' + */ + name: StringName; + }; + }; + }; + /** + * Should the exports from the generated files be re-exported in the index + * barrel file? + * + * @default false + */ + exportFromIndex: boolean; + /** + * Enable Arktype metadata support? It's often useful to associate a schema with + * some additional metadata for documentation, code generation, AI + * structured outputs, form validation, and other purposes. + * + * @default false + */ + metadata: boolean; + /** + * Configuration for request-specific Arktype schemas. + * + * Controls generation of Arktype schemas for request bodies, query parameters, path + * parameters, and headers. + */ + requests: { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate Arktype schemas for request definitions. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated schema names. The name variable is + * obtained from the operation name. + * + * @default '{{name}}Data' + */ + name: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types: { + /** + * Configuration for `infer` types. + */ + infer: { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}Data' + */ + name: StringName; + }; + }; + }; + /** + * Configuration for response-specific Arktype schemas. + * + * Controls generation of Arktype schemas for response bodies, error responses, + * and status codes. + */ + responses: { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate Arktype schemas for response definitions. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated schema names. The name variable is + * obtained from the operation name. + * + * @default '{{name}}Response' + */ + name: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types: { + /** + * Configuration for `infer` types. + */ + infer: { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}Response' + */ + name: StringName; + }; + }; + }; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types: { + /** + * Configuration for `infer` types. + */ + infer: { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled: boolean; + }; + }; + /** + * Configuration for webhook-specific Arktype schemas. + * + * Controls generation of Arktype schemas for webhook payloads. + */ + webhooks: { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate Arktype schemas for webhook definitions. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated schema names. The name variable is + * is obtained from the webhook key. + * + * @default '{{name}}WebhookRequest' + */ + name: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types: { + /** + * Configuration for `infer` types. + */ + infer: { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}WebhookRequest' + */ + name: StringName; + }; + }; + }; + }; + +export type ArktypePlugin = DefinePlugin; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/api.ts b/packages/openapi-ts/src/plugins/arktype/v2/api.ts new file mode 100644 index 0000000000..9dfeb7bcb1 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/api.ts @@ -0,0 +1,87 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../tsc'; +// import { identifiers } from '../constants'; +import type { ValidatorArgs } from '../shared/types'; + +export const createRequestValidatorV2 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol(plugin.api.selector('data', operation.id)); + if (!symbol) return; + + // const out = User({ + // name: "Alan Turing", + // device: { + // platform: "enigma", + // versions: [0, "1", 0n] + // } + // }) + // if (out instanceof type.errors) { + // // hover out.summary to see validation errors + // console.error(out.summary) + // } else { + // // hover out to see your validated data + // console.log(`Hello, ${out.name}`) + // } + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: 'parseAsync', + // name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; + +export const createResponseValidatorV2 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol( + plugin.api.selector('responses', operation.id), + ); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: 'parseAsync', + // name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/plugin.ts b/packages/openapi-ts/src/plugins/arktype/v2/plugin.ts new file mode 100644 index 0000000000..74707ba1f5 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/plugin.ts @@ -0,0 +1,363 @@ +import { deduplicateSchema } from '../../../ir/schema'; +import type { IR } from '../../../ir/types'; +import { buildName } from '../../../openApi/shared/utils/name'; +import { tsc } from '../../../tsc'; +import { refToName } from '../../../utils/ref'; +import type { SchemaWithType } from '../../shared/types/schema'; +import { pathToSymbolResourceType } from '../../shared/utils/meta'; +import { toRefs } from '../../shared/utils/refs'; +import { exportAst } from '../shared/export'; +import type { Ast, IrSchemaToAstOptions } from '../shared/types'; +import type { ArktypePlugin } from '../types'; +import { irSchemaWithTypeToAst } from './toAst'; + +export const irSchemaToAst = ({ + // optional, + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + /** + * Accept `optional` to handle optional object properties. We can't handle + * this inside the object function because `.optional()` must come before + * `.default()` which is handled in this function. + */ + optional?: boolean; + schema: IR.SchemaObject; +}): Ast => { + let ast: Partial = {}; + + // const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (schema.$ref) { + const selector = plugin.api.selector('ref', schema.$ref); + const refSymbol = plugin.referenceSymbol(selector); + if (plugin.isSymbolRegistered(selector)) { + const ref = tsc.identifier({ text: refSymbol.placeholder }); + ast.expression = ref; + } else { + const lazyExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + // expression: z.placeholder, + expression: 'TODO', + name: 'TODO', + // name: identifiers.lazy, + }), + parameters: [ + tsc.arrowFunction({ + returnType: tsc.keywordTypeNode({ keyword: 'any' }), + statements: [ + tsc.returnStatement({ + expression: tsc.identifier({ text: refSymbol.placeholder }), + }), + ], + }), + ], + }); + ast.expression = lazyExpression; + ast.hasLazyExpression = true; + state.hasLazyExpression.value = true; + } + } else if (schema.type) { + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: schema as SchemaWithType, + state, + }); + ast.def = typeAst.def; + ast.expression = typeAst.expression; + ast.hasLazyExpression = typeAst.hasLazyExpression; + + if (plugin.config.metadata && schema.description) { + // TODO: add description + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: ast.expression, + // name: identifiers.register, + // }), + // parameters: [ + // tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: identifiers.globalRegistry, + // }), + // tsc.objectExpression({ + // obj: [ + // { + // key: 'description', + // value: tsc.stringLiteral({ text: schema.description }), + // }, + // ], + // }), + // ], + // }); + } + } else if (schema.items) { + schema = deduplicateSchema({ schema }); + + if (schema.items) { + // const itemSchemas = schema.items.map((item, index) => + // irSchemaToAst({ + // plugin, + // schema: item, + // state: { + // ...state, + // _path: [...state._path, 'items', index], + // }, + // }), + // ); + // 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') + // ) { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: identifiers.intersection, + // }), + // parameters: itemSchemas.map((schema) => schema.expression), + // }); + // } else { + // ast.expression = itemSchemas[0]!.expression; + // itemSchemas.slice(1).forEach((schema) => { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: ast.expression!, + // name: identifiers.and, + // }), + // parameters: [ + // schema.hasCircularReference + // ? tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: identifiers.lazy, + // }), + // parameters: [ + // tsc.arrowFunction({ + // statements: [ + // tsc.returnStatement({ + // expression: schema.expression, + // }), + // ], + // }), + // ], + // }) + // : schema.expression, + // ], + // }); + // }); + // } + // } else { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: identifiers.union, + // }), + // parameters: [ + // tsc.arrayLiteralExpression({ + // elements: itemSchemas.map((schema) => schema.expression), + // }), + // ], + // }); + // } + } else { + ast = irSchemaToAst({ plugin, schema, state }); + } + } else { + // catch-all fallback for failed schemas + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + ast.def = typeAst.def; + ast.expression = typeAst.expression; + } + + // TODO: remove later + if (!ast.expression) { + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + ast.expression = typeAst.expression; + } + // END TODO: remove later + + // if (ast.expression) { + // if (schema.accessScope === 'read') { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: ast.expression, + // name: identifiers.readonly, + // }), + // }); + // } + + // if (optional) { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: identifiers.optional, + // }), + // parameters: [ast.expression], + // }); + // ast.typeName = identifiers.ZodOptional; + // } + + // if (schema.default !== undefined) { + // const isBigInt = schema.type === 'integer' && schema.format === 'int64'; + // const callParameter = numberParameter({ + // isBigInt, + // value: schema.default, + // }); + // if (callParameter) { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: ast.expression, + // name: identifiers.default, + // }), + // parameters: [callParameter], + // }); + // } + // } + // } + + return ast as Ast; +}; + +const handleComponent = ({ + $ref, + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + $ref: string; + schema: IR.SchemaObject; +}): void => { + const ast = irSchemaToAst({ plugin, schema, state }); + const baseName = refToName($ref); + const resourceType = pathToSymbolResourceType(state._path.value); + const symbol = plugin.registerSymbol({ + exported: true, + meta: { + resourceType, + }, + name: buildName({ + config: plugin.config.definitions, + name: baseName, + }), + selector: plugin.api.selector('ref', $ref), + }); + const typeInferSymbol = plugin.config.definitions.types.infer.enabled + ? plugin.registerSymbol({ + exported: true, + meta: { + kind: 'type', + resourceType, + }, + name: buildName({ + config: plugin.config.definitions.types.infer, + name: baseName, + }), + selector: plugin.api.selector('type-infer-ref', $ref), + }) + : undefined; + exportAst({ + ast, + plugin, + schema, + symbol, + typeInferSymbol, + }); +}; + +export const handlerV2: ArktypePlugin['Handler'] = ({ plugin }) => { + plugin.registerSymbol({ + external: 'arktype', + name: 'type', + selector: plugin.api.selector('external', 'arktype.type'), + }); + + plugin.forEach( + 'operation', + 'parameter', + 'requestBody', + 'schema', + 'webhook', + (event) => { + switch (event.type) { + // case 'operation': + // operationToZodSchema({ + // getZodSchema: (schema) => { + // const state: State = { + // circularReferenceTracker: [], + // currentReferenceTracker: [], + // hasCircularReference: false, + // }; + // return schemaToZodSchema({ plugin, schema, state }); + // }, + // operation: event.operation, + // plugin, + // }); + // break; + case 'parameter': + handleComponent({ + $ref: event.$ref, + plugin, + schema: event.parameter.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), + }); + break; + case 'requestBody': + handleComponent({ + $ref: event.$ref, + plugin, + schema: event.requestBody.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), + }); + break; + case 'schema': + handleComponent({ + $ref: event.$ref, + plugin, + schema: event.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), + }); + break; + // case 'webhook': + // webhookToZodSchema({ + // getZodSchema: (schema) => { + // const state: State = { + // circularReferenceTracker: [], + // currentReferenceTracker: [], + // hasCircularReference: false, + // }; + // return schemaToZodSchema({ plugin, schema, state }); + // }, + // operation: event.operation, + // plugin, + // }); + // break; + } + }, + ); +}; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts b/packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts new file mode 100644 index 0000000000..9574825f69 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts @@ -0,0 +1,123 @@ +import ts from 'typescript'; + +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { nullToAst } from './null'; +import { objectToAst } from './object'; +import { stringToAst } from './string'; +// import { arrayToAst } from "./array"; +// import { booleanToAst } from "./boolean"; +// import { enumToAst } from "./enum"; +// import { neverToAst } from "./never"; +// import { numberToAst } from "./number"; +// import { tupleToAst } from "./tuple"; +// import { undefinedToAst } from "./undefined"; +// import { unknownToAst } from "./unknown"; +// import { voidToAst } from "./void"; + +export const irSchemaWithTypeToAst = ({ + schema, + ...args +}: IrSchemaToAstOptions & { + schema: SchemaWithType; +}): Omit => { + switch (schema.type) { + // case 'array': + // return arrayToAst({ + // ...args, + // schema: schema as SchemaWithType<'array'>, + // }); + // case 'boolean': + // return booleanToAst({ + // ...args, + // schema: schema as SchemaWithType<'boolean'>, + // }); + // case 'enum': + // return enumToAst({ + // ...args, + // schema: schema as SchemaWithType<'enum'>, + // }); + // case 'integer': + // case 'number': + // return numberToAst({ + // ...args, + // schema: schema as SchemaWithType<'integer' | 'number'>, + // }); + // case 'never': + // return neverToAst({ + // ...args, + // schema: schema as SchemaWithType<'never'>, + // }); + case 'null': + return nullToAst({ + ...args, + schema: schema as SchemaWithType<'null'>, + }); + case 'object': + return objectToAst({ + ...args, + schema: schema as SchemaWithType<'object'>, + }); + case 'string': + return stringToAst({ + ...args, + schema: schema as SchemaWithType<'string'>, + }); + // case 'tuple': + // return tupleToAst({ + // ...args, + // schema: schema as SchemaWithType<'tuple'>, + // }); + // case 'undefined': + // return undefinedToAst({ + // ...args, + // schema: schema as SchemaWithType<'undefined'>, + // }); + // case 'unknown': + // return unknownToAst({ + // ...args, + // schema: schema as SchemaWithType<'unknown'>, + // }); + // case 'void': + // return voidToAst({ + // ...args, + // schema: schema as SchemaWithType<'void'>, + // }); + } + + const type = args.plugin.referenceSymbol( + args.plugin.api.selector('external', 'arktype.type'), + ); + + const expression = ts.factory.createCallExpression( + ts.factory.createIdentifier(type.placeholder), + undefined, + [ + ts.factory.createObjectLiteralExpression( + [ + ts.factory.createPropertyAssignment( + 'name', + ts.factory.createStringLiteral('string'), + ), + ts.factory.createPropertyAssignment( + 'platform', + ts.factory.createStringLiteral("'android' | 'ios'"), + ), + ts.factory.createPropertyAssignment( + ts.factory.createComputedPropertyName( + ts.factory.createStringLiteral('versions?'), + ), + ts.factory.createStringLiteral('(number | string)[]'), + ), + ], + true, + ), + ], + ); + + return { + def: '', + expression, + hasLazyExpression: false, + }; +}; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/toAst/null.ts b/packages/openapi-ts/src/plugins/arktype/v2/toAst/null.ts new file mode 100644 index 0000000000..ba1ce6731f --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/toAst/null.ts @@ -0,0 +1,14 @@ +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const nullToAst = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _args: IrSchemaToAstOptions & { + schema: SchemaWithType<'null'>; + }, +): Omit => { + const result: Partial> = {}; + result.def = identifiers.primitives.null; + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts b/packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts new file mode 100644 index 0000000000..b536081df0 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts @@ -0,0 +1,171 @@ +import ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import { numberRegExp } from '../../../../utils/regexp'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +// import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const objectToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'object'>; +}): Omit => { + const result: Partial> = {}; + + // TODO: parser - handle constants + const properties: Array = + []; + + const required = schema.required ?? []; + + for (const name in schema.properties) { + const property = schema.properties[name]!; + const isRequired = required.includes(name); + + const propertyAst = irSchemaToAst({ + optional: !isRequired, + plugin, + schema: property, + state: { + ...state, + _path: toRef([...state._path.value, 'properties', name]), + }, + }); + if (propertyAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + + let propertyName: + | ts.ComputedPropertyName + | ts.StringLiteral + | ts.NumericLiteral + | string = isRequired ? name : `${name}?`; + + // if (propertyAst.hasCircularReference) { + // properties.push( + // tsc.getAccessorDeclaration({ + // name: propertyName, + // // @ts-expect-error + // returnType: propertyAst.typeName + // ? tsc.propertyAccessExpression({ + // expression: 'TODO', + // name: propertyAst.typeName, + // }) + // : undefined, + // statements: [ + // tsc.returnStatement({ + // expression: propertyAst.expression, + // }), + // ], + // }), + // ); + // } else { + // properties.push( + // tsc.propertyAssignment({ + // initializer: propertyAst.expression, + // name: ts.factory.createComputedPropertyName( + // ts.factory.createStringLiteral(`${propertyName}?`), + // ), + // }), + // ); + // } + + if (propertyName.endsWith('?')) { + propertyName = ts.factory.createComputedPropertyName( + tsc.stringLiteral({ text: propertyName }), + ); + } else { + // TODO: parser - abstract safe property name logic + if ( + ((propertyName.match(/^[0-9]/) && propertyName.match(/\D+/g)) || + propertyName.match(/\W/g)) && + !propertyName.startsWith("'") && + !propertyName.endsWith("'") + ) { + propertyName = `'${propertyName}'`; + } + + numberRegExp.lastIndex = 0; + if (numberRegExp.test(propertyName)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = propertyName.startsWith('-') + ? tsc.stringLiteral({ text: name }) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = name; + } + } + properties.push( + tsc.propertyAssignment({ + initializer: propertyAst.expression, + name: propertyName, + }), + ); + } + + if ( + schema.additionalProperties && + (!schema.properties || !Object.keys(schema.properties).length) + ) { + const additionalAst = irSchemaToAst({ + plugin, + schema: schema.additionalProperties, + state: { + ...state, + _path: toRef([...state._path.value, 'additionalProperties']), + }, + }); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: 'TODO', + name: 'record', + // name: identifiers.record, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: 'TODO', + name: 'string', + // name: identifiers.string, + }), + parameters: [], + }), + additionalAst.expression, + ], + }); + if (additionalAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + + // Return with typeName for circular references + if (result.hasLazyExpression) { + return { + ...result, + typeName: 'TODO', + } as Ast; + } + + return result as Omit; + } + + result.expression = ts.factory.createObjectLiteralExpression( + properties, + true, + ); + + // return with typeName for circular references + if (result.hasLazyExpression) { + return { + ...result, + typeName: 'TODO', + } as Ast; + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/toAst/string.ts b/packages/openapi-ts/src/plugins/arktype/v2/toAst/string.ts new file mode 100644 index 0000000000..9395de267d --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/toAst/string.ts @@ -0,0 +1,65 @@ +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const stringToAst = ({ + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'string'>; +}): Omit => { + const result: Partial> = {}; + + if (typeof schema.const === 'string') { + result.def = schema.const; + return result as Omit; + } + + let def = identifiers.primitives.string; + + if (schema.format) { + switch (schema.format) { + case 'date': + case 'date-time': + case 'time': + def = `${def}.${identifiers.string.date}.${identifiers.string.iso}`; + break; + case 'email': + def = `${def}.${identifiers.string.email}`; + break; + case 'ipv4': + def = `${def}.${identifiers.string.ip}.${identifiers.string.v4}`; + break; + case 'ipv6': + def = `${def}.${identifiers.string.ip}.${identifiers.string.v6}`; + break; + case 'uri': + def = `${def}.${identifiers.string.url}`; + break; + case 'uuid': + def = `${def}.${identifiers.string.uuid}`; + break; + } + } + + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { + def = `${schema.minLength} <= ${def} <= ${schema.maxLength}`; + } else { + if (schema.maxLength !== undefined) { + def = `${def} <= ${schema.maxLength}`; + + if (schema.minLength !== undefined) { + def = `${schema.minLength} <= ${def}`; + } + } else if (schema.minLength !== undefined) { + def = `${def} >= ${schema.minLength}`; + } + } + + if (schema.pattern) { + def = `/${schema.pattern}/`; + } + + result.def = def; + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/config.ts b/packages/openapi-ts/src/plugins/config.ts index d8460ececc..46002cb6ee 100644 --- a/packages/openapi-ts/src/plugins/config.ts +++ b/packages/openapi-ts/src/plugins/config.ts @@ -42,6 +42,8 @@ import type { TanStackSvelteQueryPlugin } from './@tanstack/svelte-query'; import { defaultConfig as tanStackSvelteQuery } from './@tanstack/svelte-query'; import type { TanStackVueQueryPlugin } from './@tanstack/vue-query'; import { defaultConfig as tanStackVueQuery } from './@tanstack/vue-query'; +import type { ArktypePlugin } from './arktype'; +import { defaultConfig as arktype } from './arktype'; import type { FastifyPlugin } from './fastify'; import { defaultConfig as fastify } from './fastify'; import type { Plugin, PluginNames } from './types'; @@ -68,6 +70,7 @@ export interface PluginConfigMap { '@tanstack/solid-query': TanStackSolidQueryPlugin['Types']; '@tanstack/svelte-query': TanStackSvelteQueryPlugin['Types']; '@tanstack/vue-query': TanStackVueQueryPlugin['Types']; + arktype: ArktypePlugin['Types']; fastify: FastifyPlugin['Types']; 'legacy/angular': HeyApiClientLegacyAngularPlugin['Types']; 'legacy/axios': HeyApiClientLegacyAxiosPlugin['Types']; @@ -98,6 +101,7 @@ export const defaultPluginConfigs: { '@tanstack/solid-query': tanStackSolidQuery, '@tanstack/svelte-query': tanStackSvelteQuery, '@tanstack/vue-query': tanStackVueQuery, + arktype, fastify, 'legacy/angular': heyApiLegacyAngular, 'legacy/axios': heyApiLegacyAxios, diff --git a/packages/openapi-ts/src/plugins/fastify/api.ts b/packages/openapi-ts/src/plugins/fastify/api.ts index b8b2cd8f7c..eb1e5f9c19 100644 --- a/packages/openapi-ts/src/plugins/fastify/api.ts +++ b/packages/openapi-ts/src/plugins/fastify/api.ts @@ -11,13 +11,13 @@ export type IApi = { * - `RouteHandler`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'fastify'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/fastify/plugin.ts b/packages/openapi-ts/src/plugins/fastify/plugin.ts index 7c79bcbdca..6176932392 100644 --- a/packages/openapi-ts/src/plugins/fastify/plugin.ts +++ b/packages/openapi-ts/src/plugins/fastify/plugin.ts @@ -17,7 +17,7 @@ const operationToRouteHandler = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); if (symbolDataType) { if (operation.body) { @@ -65,7 +65,7 @@ const operationToRouteHandler = ({ let errorsTypeReference: ts.TypeReferenceNode | undefined = undefined; const symbolErrorType = plugin.getSymbol( - pluginTypeScript.api.getSelector('errors', operation.id), + pluginTypeScript.api.selector('errors', operation.id), ); if (symbolErrorType && errors && errors.properties) { const keys = Object.keys(errors.properties); @@ -92,7 +92,7 @@ const operationToRouteHandler = ({ let responsesTypeReference: ts.TypeReferenceNode | undefined = undefined; const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector('responses', operation.id), + pluginTypeScript.api.selector('responses', operation.id), ); if (symbolResponseType && responses && responses.properties) { const keys = Object.keys(responses.properties); @@ -134,7 +134,7 @@ const operationToRouteHandler = ({ } const symbolRouteHandler = plugin.referenceSymbol( - plugin.api.getSelector('RouteHandler'), + plugin.api.selector('RouteHandler'), ); const routeHandler: Property = { name: operation.id, @@ -158,7 +158,7 @@ export const handler: FastifyPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'RouteHandler', - selector: plugin.api.getSelector('RouteHandler'), + selector: plugin.api.selector('RouteHandler'), }); const symbolRouteHandlers = plugin.registerSymbol({ @@ -171,12 +171,18 @@ export const handler: FastifyPlugin['Handler'] = ({ plugin }) => { const routeHandlers: Array = []; - plugin.forEach('operation', ({ operation }) => { - const routeHandler = operationToRouteHandler({ operation, plugin }); - if (routeHandler) { - routeHandlers.push(routeHandler); - } - }); + plugin.forEach( + 'operation', + ({ operation }) => { + const routeHandler = operationToRouteHandler({ operation, plugin }); + if (routeHandler) { + routeHandlers.push(routeHandler); + } + }, + { + order: 'declarations', + }, + ); const node = tsc.typeAliasDeclaration({ exportType: symbolRouteHandlers.exported, diff --git a/packages/openapi-ts/src/plugins/shared/types/instance.d.ts b/packages/openapi-ts/src/plugins/shared/types/instance.d.ts index 7527eb57b4..c8e770f3fd 100644 --- a/packages/openapi-ts/src/plugins/shared/types/instance.d.ts +++ b/packages/openapi-ts/src/plugins/shared/types/instance.d.ts @@ -1,3 +1,4 @@ +import type { IrTopLevelKind } from '../../../ir/graph'; import type { IR } from '../../../ir/types'; type WalkEvents = @@ -6,44 +7,62 @@ type WalkEvents = method: keyof IR.PathItemObject; operation: IR.OperationObject; path: string; - type: 'operation'; + type: Extract; } | { $ref: string; _path: ReadonlyArray; name: string; parameter: IR.ParameterObject; - type: 'parameter'; + type: Extract; } | { $ref: string; _path: ReadonlyArray; name: string; requestBody: IR.RequestBodyObject; - type: 'requestBody'; + type: Extract; } | { $ref: string; _path: ReadonlyArray; name: string; schema: IR.SchemaObject; - type: 'schema'; + type: Extract; } | { _path: ReadonlyArray; server: IR.ServerObject; - type: 'server'; + type: Extract; } | { _path: ReadonlyArray; key: string; method: keyof IR.PathItemObject; operation: IR.OperationObject; - type: 'webhook'; + type: Extract; }; -export type WalkEventType = WalkEvents['type']; -export type WalkEvent = Extract< +export type WalkEvent = Extract< WalkEvents, { type: T } >; + +export type WalkOptions = { + /** + * Order of walking schemas. + * + * The "declarations" option ensures that schemas are walked in the order + * they are declared in the input document. This is useful for scenarios where + * the order of declaration matters, such as when generating code that relies + * on the sequence of schema definitions. + * + * The "topological" option ensures that schemas are walked in an order + * where dependencies are visited before the schemas that depend on them. + * This is useful for scenarios where you need to process or generate + * schemas in a way that respects their interdependencies. + * + * @default 'topological' + */ + order?: 'declarations' | 'topological'; +}; diff --git a/packages/openapi-ts/src/plugins/shared/types/refs.d.ts b/packages/openapi-ts/src/plugins/shared/types/refs.d.ts new file mode 100644 index 0000000000..79c839311d --- /dev/null +++ b/packages/openapi-ts/src/plugins/shared/types/refs.d.ts @@ -0,0 +1,59 @@ +/** + * Ref wrapper which ensures a stable reference for a value. + * + * @example + * ```ts + * type NumRef = Ref; // { value: number } + * const num: NumRef = { value: 42 }; + * console.log(num.value); // 42 + * ``` + */ +type Ref = { value: T }; + +/** + * Utility type: wraps a value in a Ref. + * + * @example + * ```ts + * type R = ToRef; // { value: number } + * ``` + */ +export type ToRef = Ref; + +/** + * Utility type: unwraps a Ref to its value type. + * @example + * ```ts + * type N = FromRef<{ value: number }>; // number + * ``` + */ +export type FromRef = T extends Ref ? V : T; + +/** + * Maps every property of a Ref-wrapped object back to its plain value. + * + * @example + * ```ts + * type Foo = { a: number; b: string }; + * type Refs = ToRefs; // { a: Ref; b: Ref } + * type Foo2 = FromRefs; // { a: number; b: string } + * ``` + */ +export type FromRefs = { + [K in keyof T]: T[K] extends Ref ? V : T[K]; +}; + +/** + * Maps every property of `T` to a `Ref` of that property. + * + * @example + * ```ts + * type Foo = { a: number; b: string }; + * type Refs = ToRefs; // { a: Ref; b: Ref } + * const refs: Refs = { a: { value: 1 }, b: { value: 'x' } }; + * console.log(refs.a.value, refs.b.value); // 1 'x' + * ``` + */ +export type ToRefs = { + [K in keyof T]: Ref; +}; diff --git a/packages/openapi-ts/src/plugins/shared/types/schema.d.ts b/packages/openapi-ts/src/plugins/shared/types/schema.d.ts new file mode 100644 index 0000000000..7123684284 --- /dev/null +++ b/packages/openapi-ts/src/plugins/shared/types/schema.d.ts @@ -0,0 +1,8 @@ +import type { IR } from '../../../ir/types'; + +export interface SchemaWithType< + T extends + Required['type'] = Required['type'], +> extends Omit { + type: Extract['type'], T>; +} diff --git a/packages/openapi-ts/src/plugins/shared/utils/__tests__/refs.test.ts b/packages/openapi-ts/src/plugins/shared/utils/__tests__/refs.test.ts new file mode 100644 index 0000000000..afc221d630 --- /dev/null +++ b/packages/openapi-ts/src/plugins/shared/utils/__tests__/refs.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; + +import { fromRef, fromRefs, toRef, toRefs } from '../refs'; + +describe('toRef', () => { + it('wraps a primitive value', () => { + expect(toRef(42)).toEqual({ value: 42 }); + expect(toRef('x')).toEqual({ value: 'x' }); + }); + + it('wraps an object', () => { + const obj = { foo: 1 }; + expect(toRef(obj)).toEqual({ value: obj }); + }); +}); + +describe('fromRef', () => { + it('unwraps a primitive value', () => { + expect(fromRef({ value: 42 })).toBe(42); + expect(fromRef({ value: 'x' })).toBe('x'); + }); + + it('unwraps an object', () => { + const obj = { foo: 1 }; + expect(fromRef({ value: obj })).toBe(obj); + }); +}); + +describe('toRef <-> fromRef roundtrip', () => { + it('roundtrips value -> ref -> value', () => { + expect(fromRef(toRef(123))).toBe(123); + const obj = { foo: 'bar' }; + expect(fromRef(toRef(obj))).toBe(obj); + }); +}); + +describe('toRefs', () => { + it('wraps primitives', () => { + expect(toRefs({ a: 1, b: 'x' })).toEqual({ + a: { value: 1 }, + b: { value: 'x' }, + }); + }); + + it('wraps empty object', () => { + expect(toRefs({})).toEqual({}); + }); + + it('wraps nested objects shallowly', () => { + const input = { a: { foo: 1 }, b: [1, 2] }; + const refs = toRefs(input); + expect(refs.a.value).toEqual({ foo: 1 }); + expect(refs.b.value).toEqual([1, 2]); + }); +}); + +describe('fromRefs', () => { + it('unwraps primitives', () => { + expect(fromRefs({ a: { value: 1 }, b: { value: 'x' } })).toEqual({ + a: 1, + b: 'x', + }); + }); + + it('unwraps empty object', () => { + expect(fromRefs({})).toEqual({}); + }); + + it('unwraps nested objects shallowly', () => { + const input = { a: { value: { foo: 1 } }, b: { value: [1, 2] } }; + expect(fromRefs(input)).toEqual({ a: { foo: 1 }, b: [1, 2] }); + }); +}); + +describe('toRefs <-> fromRefs roundtrip', () => { + it('roundtrips plain -> refs -> plain', () => { + const obj = { a: 1, b: 'x', c: [1, 2], d: { foo: 2 } }; + expect(fromRefs(toRefs(obj))).toEqual(obj); + }); + + it('roundtrips refs -> plain -> refs', () => { + const refs = { a: { value: 1 }, b: { value: 'x' }, c: { value: [1, 2] } }; + expect(toRefs(fromRefs(refs))).toEqual(refs); + }); +}); diff --git a/packages/openapi-ts/src/plugins/shared/utils/instance.ts b/packages/openapi-ts/src/plugins/shared/utils/instance.ts index ae77a6e30a..619c76f47e 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/instance.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/instance.ts @@ -8,11 +8,18 @@ import type { } from '@hey-api/codegen-core'; import { HeyApiError } from '../../../error'; +import type { IrTopLevelKind } from '../../../ir/graph'; +import { + irTopLevelKinds, + matchIrTopLevelPointer, + walkTopological, +} from '../../../ir/graph'; import type { IR } from '../../../ir/types'; import type { OpenApi } from '../../../openApi/types'; +import { jsonPointerToPath } from '../../../utils/ref'; import type { PluginConfigMap } from '../../config'; import type { Plugin } from '../../types'; -import type { WalkEvent, WalkEventType } from '../types/instance'; +import type { WalkEvent, WalkOptions } from '../types/instance'; const defaultGetFilePath = (symbol: Symbol): string | undefined => { if (!symbol.meta?.pluginName || typeof symbol.meta.pluginName !== 'string') { @@ -108,108 +115,105 @@ export class PluginInstance { * } * }); */ - forEach( + forEach( ...args: [ ...events: ReadonlyArray, callback: (event: WalkEvent) => void, ] + ): void; + forEach( + ...args: [ + ...events: ReadonlyArray, + callback: (event: WalkEvent) => void, + options: WalkOptions, + ] + ): void; + forEach( + ...args: [ + ...events: ReadonlyArray, + callback: (event: WalkEvent) => void, + options: any, + ] ): void { - const events = args.slice(0, -1) as ReadonlyArray; - const callback = args[args.length - 1] as (event: WalkEvent) => void; - const eventSet = new Set( - events.length - ? events - : ([ - 'operation', - 'parameter', - 'requestBody', - 'schema', - 'server', - 'webhook', - ] as ReadonlyArray), - ); - - if (eventSet.has('server') && this.context.ir.servers) { - for (const server of this.context.ir.servers) { - const event: WalkEvent<'server'> = { - _path: ['servers', String(this.context.ir.servers.indexOf(server))], - server, - type: 'server', - }; - try { - callback(event as WalkEvent); - } catch (error) { - this.forEachError(error, event); - } - } + let callback: (event: WalkEvent) => void; + let events: ReadonlyArray; + let options: Required = { + order: 'topological', + }; + if (typeof args[args.length - 1] === 'function') { + events = args.slice(0, -1); + callback = args[args.length - 1]; + } else { + events = args.slice(0, -2); + callback = args[args.length - 2]; + options = { + ...options, + ...args[args.length - 1], + }; } + const eventSet = new Set(events.length ? events : irTopLevelKinds); - if (eventSet.has('schema') && this.context.ir.components?.schemas) { - for (const name in this.context.ir.components.schemas) { - const event: WalkEvent<'schema'> = { - $ref: `#/components/schemas/${name}`, - _path: ['components', 'schemas', name], - name, - schema: this.context.ir.components.schemas[name]!, - type: 'schema', - }; - try { - callback(event as WalkEvent); - } catch (error) { - this.forEachError(error, event); + if (options.order === 'declarations') { + if (eventSet.has('server') && this.context.ir.servers) { + for (const server of this.context.ir.servers) { + const event: WalkEvent<'server'> = { + _path: ['servers', String(this.context.ir.servers.indexOf(server))], + server, + type: 'server', + }; + try { + callback(event as WalkEvent); + } catch (error) { + this.forEachError(error, event); + } } } - } - if (eventSet.has('parameter') && this.context.ir.components?.parameters) { - for (const name in this.context.ir.components.parameters) { - const event: WalkEvent<'parameter'> = { - $ref: `#/components/parameters/${name}`, - _path: ['components', 'parameters', name], - name, - parameter: this.context.ir.components.parameters[name]!, - type: 'parameter', - }; - try { - callback(event as WalkEvent); - } catch (error) { - this.forEachError(error, event); + if (eventSet.has('schema') && this.context.ir.components?.schemas) { + for (const name in this.context.ir.components.schemas) { + const event: WalkEvent<'schema'> = { + $ref: `#/components/schemas/${name}`, + _path: ['components', 'schemas', name], + name, + schema: this.context.ir.components.schemas[name]!, + type: 'schema', + }; + try { + callback(event as WalkEvent); + } catch (error) { + this.forEachError(error, event); + } } } - } - if ( - eventSet.has('requestBody') && - this.context.ir.components?.requestBodies - ) { - for (const name in this.context.ir.components.requestBodies) { - const event: WalkEvent<'requestBody'> = { - $ref: `#/components/requestBodies/${name}`, - _path: ['components', 'requestBodies', name], - name, - requestBody: this.context.ir.components.requestBodies[name]!, - type: 'requestBody', - }; - try { - callback(event as WalkEvent); - } catch (error) { - this.forEachError(error, event); + if (eventSet.has('parameter') && this.context.ir.components?.parameters) { + for (const name in this.context.ir.components.parameters) { + const event: WalkEvent<'parameter'> = { + $ref: `#/components/parameters/${name}`, + _path: ['components', 'parameters', name], + name, + parameter: this.context.ir.components.parameters[name]!, + type: 'parameter', + }; + try { + callback(event as WalkEvent); + } catch (error) { + this.forEachError(error, event); + } } } - } - if (eventSet.has('operation') && this.context.ir.paths) { - for (const path in this.context.ir.paths) { - const pathItem = - this.context.ir.paths[path as keyof typeof this.context.ir.paths]; - for (const _method in pathItem) { - const method = _method as keyof typeof pathItem; - const event: WalkEvent<'operation'> = { - _path: ['paths', path, method], - method, - operation: pathItem[method]!, - path, - type: 'operation', + if ( + eventSet.has('requestBody') && + this.context.ir.components?.requestBodies + ) { + for (const name in this.context.ir.components.requestBodies) { + const event: WalkEvent<'requestBody'> = { + $ref: `#/components/requestBodies/${name}`, + _path: ['components', 'requestBodies', name], + name, + requestBody: this.context.ir.components.requestBodies[name]!, + type: 'requestBody', }; try { callback(event as WalkEvent); @@ -218,27 +222,116 @@ export class PluginInstance { } } } - } - if (eventSet.has('webhook') && this.context.ir.webhooks) { - for (const key in this.context.ir.webhooks) { - const webhook = this.context.ir.webhooks[key]; - for (const _method in webhook) { - const method = _method as keyof typeof webhook; - const event: WalkEvent<'webhook'> = { - _path: ['webhooks', key, method], - key, - method, - operation: webhook[method]!, - type: 'webhook', - }; + if (eventSet.has('operation') && this.context.ir.paths) { + for (const path in this.context.ir.paths) { + const pathItem = + this.context.ir.paths[path as keyof typeof this.context.ir.paths]; + for (const _method in pathItem) { + const method = _method as keyof typeof pathItem; + const event: WalkEvent<'operation'> = { + _path: ['paths', path, method], + method, + operation: pathItem[method]!, + path, + type: 'operation', + }; + try { + callback(event as WalkEvent); + } catch (error) { + this.forEachError(error, event); + } + } + } + } + + if (eventSet.has('webhook') && this.context.ir.webhooks) { + for (const key in this.context.ir.webhooks) { + const webhook = this.context.ir.webhooks[key]; + for (const _method in webhook) { + const method = _method as keyof typeof webhook; + const event: WalkEvent<'webhook'> = { + _path: ['webhooks', key, method], + key, + method, + operation: webhook[method]!, + type: 'webhook', + }; + try { + callback(event as WalkEvent); + } catch (error) { + this.forEachError(error, event); + } + } + } + } + } else if (options.order === 'topological' && this.context.graph) { + walkTopological(this.context.graph, (pointer, nodeInfo) => { + const result = matchIrTopLevelPointer(pointer); + if (!result.matched || !eventSet.has(result.kind)) return; + let event: WalkEvent | undefined; + switch (result.kind) { + case 'operation': + event = { + _path: jsonPointerToPath(pointer), + method: nodeInfo.key as keyof IR.PathItemObject, + operation: nodeInfo.node as IR.OperationObject, + path: jsonPointerToPath(pointer)[1]!, + type: result.kind, + } satisfies WalkEvent<'operation'>; + break; + case 'parameter': + event = { + $ref: pointer, + _path: jsonPointerToPath(pointer), + name: nodeInfo.key as string, + parameter: nodeInfo.node as IR.ParameterObject, + type: result.kind, + } satisfies WalkEvent<'parameter'>; + break; + case 'requestBody': + event = { + $ref: pointer, + _path: jsonPointerToPath(pointer), + name: nodeInfo.key as string, + requestBody: nodeInfo.node as IR.RequestBodyObject, + type: result.kind, + } satisfies WalkEvent<'requestBody'>; + break; + case 'schema': + event = { + $ref: pointer, + _path: jsonPointerToPath(pointer), + name: nodeInfo.key as string, + schema: nodeInfo.node as IR.SchemaObject, + type: result.kind, + } satisfies WalkEvent<'schema'>; + break; + case 'server': + event = { + _path: jsonPointerToPath(pointer), + server: nodeInfo.node as IR.ServerObject, + type: result.kind, + } satisfies WalkEvent<'server'>; + break; + case 'webhook': + event = { + _path: jsonPointerToPath(pointer), + key: jsonPointerToPath(pointer)[1]!, + method: nodeInfo.key as keyof IR.PathItemObject, + operation: nodeInfo.node as IR.OperationObject, + type: result.kind, + } satisfies WalkEvent<'webhook'>; + break; + } + if (event) { try { callback(event as WalkEvent); } catch (error) { this.forEachError(error, event); } } - } + }); } } @@ -354,6 +447,10 @@ export class PluginInstance { return (defaultGetKind(operation) ?? []).includes(kind); } + isSymbolRegistered(symbolIdOrSelector: number | Selector): boolean { + return this.gen.symbols.isRegistered(symbolIdOrSelector); + } + referenceSymbol(symbolIdOrSelector: number | Selector): Symbol { return this.gen.symbols.reference(symbolIdOrSelector); } diff --git a/packages/openapi-ts/src/plugins/shared/utils/refs.ts b/packages/openapi-ts/src/plugins/shared/utils/refs.ts new file mode 100644 index 0000000000..6161369066 --- /dev/null +++ b/packages/openapi-ts/src/plugins/shared/utils/refs.ts @@ -0,0 +1,66 @@ +import type { FromRefs, ToRefs } from '../types/refs'; + +/** + * Wraps a single value in a Ref object. + * + * @example + * ```ts + * const r = toRef(123); // { value: 123 } + * console.log(r.value); // 123 + * ``` + */ +export const toRef = (value: T): { value: T } => ({ value }); + +/** + * Unwraps a single Ref object to its value. + * + * @example + * ```ts + * const r = { value: 42 }; + * const n = fromRef(r); // 42 + * console.log(n); // 42 + * ``` + */ +export const fromRef = (ref: { value: T }): T => ref.value; + +/** + * Converts an object of Refs back to a plain object (unwraps all refs). + * + * @example + * ```ts + * const refs = { a: { value: 1 }, b: { value: "x" } }; + * const plain = fromRefs(refs); // { a: 1, b: "x" } + * ``` + */ +export const fromRefs = >>( + obj: T, +): FromRefs => { + const result = {} as FromRefs; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + result[key] = fromRef(obj[key]!) as (typeof result)[typeof key]; + } + } + return result; +}; + +/** + * Converts a plain object to an object of Refs (deep, per property). + * + * @example + * ```ts + * const obj = { a: 1, b: "x" }; + * const refs = toRefs(obj); // { a: { value: 1 }, b: { value: "x" } } + * ``` + */ +export const toRefs = >( + obj: T, +): ToRefs => { + const result = {} as ToRefs; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + result[key] = toRef(obj[key]); + } + } + return result; +}; diff --git a/packages/openapi-ts/src/plugins/types.d.ts b/packages/openapi-ts/src/plugins/types.d.ts index 79f92e2202..5d3172ab58 100644 --- a/packages/openapi-ts/src/plugins/types.d.ts +++ b/packages/openapi-ts/src/plugins/types.d.ts @@ -19,7 +19,7 @@ export type PluginClientNames = | 'legacy/node' | 'legacy/xhr'; -export type PluginValidatorNames = 'valibot' | 'zod'; +export type PluginValidatorNames = 'arktype' | 'valibot' | 'zod'; export type PluginNames = | PluginClientNames diff --git a/packages/openapi-ts/src/plugins/valibot/api.ts b/packages/openapi-ts/src/plugins/valibot/api.ts index be4b955e83..f59ae2b246 100644 --- a/packages/openapi-ts/src/plugins/valibot/api.ts +++ b/packages/openapi-ts/src/plugins/valibot/api.ts @@ -1,18 +1,16 @@ import type { Selector } from '@hey-api/codegen-core'; import type ts from 'typescript'; -import type { IR } from '../../ir/types'; -import { tsc } from '../../tsc'; import type { Plugin } from '../types'; -import { identifiers } from './constants'; -import type { ValibotPlugin } from './types'; +import type { ValidatorArgs } from './shared/types'; +import { createRequestValidatorV1, createResponseValidatorV1 } from './v1/api'; -type SelectorType = 'data' | 'import' | 'ref' | 'responses' | 'webhook-request'; - -type ValidatorArgs = { - operation: IR.OperationObject; - plugin: ValibotPlugin['Instance']; -}; +type SelectorType = + | 'data' + | 'external' + | 'ref' + | 'responses' + | 'webhook-request'; export type IApi = { createRequestValidator: (args: ValidatorArgs) => ts.ArrowFunction | undefined; @@ -23,101 +21,27 @@ export type IApi = { * @param type Selector type. * @param value Depends on `type`: * - `data`: `operation.id` string - * - `import`: headless symbols representing module imports + * - `external`: external modules * - `ref`: `$ref` JSON pointer * - `responses`: `operation.id` string * - `webhook-request`: `operation.id` string * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'valibot'>) {} - createRequestValidator({ - operation, - plugin, - }: ValidatorArgs): ts.ArrowFunction | undefined { - const symbol = plugin.getSymbol( - plugin.api.getSelector('data', operation.id), - ); - if (!symbol) return; - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - const dataParameterName = 'data'; - - return tsc.arrowFunction({ - async: true, - parameters: [ - { - name: dataParameterName, - }, - ], - statements: [ - tsc.returnStatement({ - expression: tsc.awaitExpression({ - expression: tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.async.parseAsync, - }), - parameters: [ - tsc.identifier({ text: symbol.placeholder }), - tsc.identifier({ text: dataParameterName }), - ], - }), - }), - }), - ], - }); + createRequestValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + return createRequestValidatorV1(args); } - createResponseValidator({ - operation, - plugin, - }: ValidatorArgs): ts.ArrowFunction | undefined { - const symbol = plugin.getSymbol( - plugin.api.getSelector('responses', operation.id), - ); - if (!symbol) return; - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - const dataParameterName = 'data'; - - return tsc.arrowFunction({ - async: true, - parameters: [ - { - name: dataParameterName, - }, - ], - statements: [ - tsc.returnStatement({ - expression: tsc.awaitExpression({ - expression: tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.async.parseAsync, - }), - parameters: [ - tsc.identifier({ text: symbol.placeholder }), - tsc.identifier({ text: dataParameterName }), - ], - }), - }), - }), - ], - }); + createResponseValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + return createResponseValidatorV1(args); } - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/valibot/plugin.ts b/packages/openapi-ts/src/plugins/valibot/plugin.ts index 00cc6814d0..4a72ae614f 100644 --- a/packages/openapi-ts/src/plugins/valibot/plugin.ts +++ b/packages/openapi-ts/src/plugins/valibot/plugin.ts @@ -1,1375 +1,4 @@ -import type { Symbol } from '@hey-api/codegen-core'; -import ts from 'typescript'; - -import { deduplicateSchema } from '../../ir/schema'; -import type { IR } from '../../ir/types'; -import { buildName } from '../../openApi/shared/utils/name'; -import { tsc } from '../../tsc'; -import type { StringCase, StringName } from '../../types/case'; -import { jsonPointerToPath, refToName } from '../../utils/ref'; -import { numberRegExp } from '../../utils/regexp'; -import { pathToSymbolResourceType } from '../shared/utils/meta'; -import { createSchemaComment } from '../shared/utils/schema'; -import type { SchemaWithType } from '../zod/shared/types'; -import { identifiers } from './constants'; -import { - INTEGER_FORMATS, - isIntegerFormat, - needsBigIntForFormat, - numberParameter, -} from './number-helpers'; -import { operationToValibotSchema } from './operation'; import type { ValibotPlugin } from './types'; -import { webhookToValibotSchema } from './webhook'; - -export interface State { - circularReferenceTracker: Set; - hasCircularReference: boolean; - nameCase: StringCase; - nameTransformer: StringName; -} - -type SchemaToValibotSchemaOptions = { - /** - * Path to the schema in the intermediary representation. - */ - _path: ReadonlyArray; - plugin: ValibotPlugin['Instance']; -}; - -const pipesToExpression = ({ - pipes, - plugin, -}: { - pipes: Array; - plugin: ValibotPlugin['Instance']; -}) => { - if (pipes.length === 1) { - return pipes[0]!; - } - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.methods.pipe, - }), - parameters: pipes, - }); - return expression; -}; - -const arrayTypeToValibotSchema = ({ - _path, - plugin, - schema, - state, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'array'>; - state: State; -}): ts.Expression => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - const functionName = tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.array, - }); - - const pipes: Array = []; - - if (!schema.items) { - const expression = tsc.callExpression({ - functionName, - parameters: [ - unknownTypeToValibotSchema({ - _path, - plugin, - schema: { - type: 'unknown', - }, - }), - ], - }); - pipes.push(expression); - } else { - schema = deduplicateSchema({ schema }); - - // at least one item is guaranteed - const itemExpressions = schema.items!.map((item, index) => { - const schemaPipes = schemaToValibotSchema({ - _path: [..._path, 'items', index], - plugin, - schema: item, - state, - }); - return pipesToExpression({ pipes: schemaPipes, plugin }); - }); - - if (itemExpressions.length === 1) { - const expression = tsc.callExpression({ - functionName, - parameters: itemExpressions, - }); - pipes.push(expression); - } else { - if (schema.logicalOperator === 'and') { - // TODO: parser - handle intersection - // return tsc.typeArrayNode( - // tsc.typeIntersectionNode({ types: itemExpressions }), - // ); - } - - // TODO: parser - handle union - // return tsc.typeArrayNode(tsc.typeUnionNode({ types: itemExpressions })); - - const expression = tsc.callExpression({ - functionName, - parameters: [ - unknownTypeToValibotSchema({ - _path, - plugin, - schema: { - type: 'unknown', - }, - }), - ], - }); - pipes.push(expression); - } - } - - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - pipes.push(expression); - } else { - if (schema.minItems !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.minLength, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - pipes.push(expression); - } - - if (schema.maxItems !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.maxLength, - }), - parameters: [tsc.valueToExpression({ value: schema.maxItems })], - }); - pipes.push(expression); - } - } - - return pipesToExpression({ pipes, plugin }); -}; - -const booleanTypeToValibotSchema = ({ - plugin, - schema, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'boolean'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - if (typeof schema.const === 'boolean') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); - return expression; - } - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.boolean, - }), - }); - return expression; -}; - -const enumTypeToValibotSchema = ({ - _path, - plugin, - schema, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'enum'>; -}): ts.CallExpression => { - const enumMembers: Array = []; - - let isNullable = false; - - for (const item of schema.items ?? []) { - // Zod supports only string enums - if (item.type === 'string' && typeof item.const === 'string') { - enumMembers.push( - tsc.stringLiteral({ - text: item.const, - }), - ); - } else if (item.type === 'null' || item.const === null) { - isNullable = true; - } - } - - if (!enumMembers.length) { - return unknownTypeToValibotSchema({ - _path, - plugin, - schema: { - type: 'unknown', - }, - }); - } - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - let resultExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.picklist, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: enumMembers, - multiLine: false, - }), - ], - }); - - if (isNullable) { - resultExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.nullable, - }), - parameters: [resultExpression], - }); - } - - return resultExpression; -}; - -const neverTypeToValibotSchema = ({ - plugin, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'never'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.never, - }), - }); - return expression; -}; - -const nullTypeToValibotSchema = ({ - plugin, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'null'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.null, - }), - }); - return expression; -}; - -const numberTypeToValibotSchema = ({ - plugin, - schema, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'integer' | 'number'>; -}) => { - const format = schema.format; - const isInteger = schema.type === 'integer'; - const isBigInt = needsBigIntForFormat(format); - const formatInfo = isIntegerFormat(format) ? INTEGER_FORMATS[format] : null; - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - // Return early if const is defined since we can create a literal type directly without additional validation - if (schema.const !== undefined && schema.const !== null) { - const constValue = schema.const; - let literalValue; - - // Case 1: Number with no format -> generate literal with the number - if (typeof constValue === 'number' && !format) { - literalValue = tsc.ots.number(constValue); - } - // Case 2: Number with format -> check if format needs BigInt, generate appropriate literal - else if (typeof constValue === 'number' && format) { - if (isBigInt) { - // Format requires BigInt, convert number to BigInt - literalValue = tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.ots.string(constValue.toString())], - }); - } else { - // Regular format, use number as-is - literalValue = tsc.ots.number(constValue); - } - } - // Case 3: Format that allows string -> generate BigInt literal (for int64/uint64 formats) - else if (typeof constValue === 'string' && isBigInt) { - // Remove 'n' suffix if present in string - const cleanString = constValue.endsWith('n') - ? constValue.slice(0, -1) - : constValue; - literalValue = tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.ots.string(cleanString)], - }); - } - // Case 4: Const is typeof bigint (literal) -> transform from literal to BigInt() - else if (typeof constValue === 'bigint') { - // Convert BigInt to string and remove 'n' suffix that toString() adds - const bigintString = constValue.toString(); - const cleanString = bigintString.endsWith('n') - ? bigintString.slice(0, -1) - : bigintString; - literalValue = tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.ots.string(cleanString)], - }); - } - // Default case: use value as-is for other types - else { - literalValue = tsc.valueToExpression({ value: constValue }); - } - - return tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.literal, - }), - parameters: [literalValue], - }); - } - - const pipes: Array = []; - - // For bigint formats (int64, uint64), create union of number, string, and bigint with transform - if (isBigInt) { - const unionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.number, - }), - }), - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.string, - }), - }), - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.bigInt, - }), - }), - ], - multiLine: false, - }), - ], - }); - pipes.push(unionExpression); - - // Add transform to convert to BigInt - const transformExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.transform, - }), - parameters: [ - tsc.arrowFunction({ - parameters: [{ name: 'x' }], - statements: tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.identifier({ text: 'x' })], - }), - }), - ], - }); - pipes.push(transformExpression); - } else { - // For regular number formats, use number schema - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.number, - }), - }); - pipes.push(expression); - } - - // Add integer validation for integer types (except when using bigint union) - if (!isBigInt && isInteger) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.integer, - }), - }); - pipes.push(expression); - } - - // Add format-specific range validations - if (formatInfo) { - const minValue = formatInfo.min; - const maxValue = formatInfo.max; - const minErrorMessage = formatInfo.minError; - const maxErrorMessage = formatInfo.maxError; - - // Add minimum value validation - const minExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.minValue, - }), - parameters: [ - isBigInt - ? tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.ots.string(minValue.toString())], - }) - : tsc.ots.number(minValue as number), - tsc.ots.string(minErrorMessage), - ], - }); - pipes.push(minExpression); - - // Add maximum value validation - const maxExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.maxValue, - }), - parameters: [ - isBigInt - ? tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.ots.string(maxValue.toString())], - }) - : tsc.ots.number(maxValue as number), - tsc.ots.string(maxErrorMessage), - ], - }); - pipes.push(maxExpression); - } - - if (schema.exclusiveMinimum !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.gtValue, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMinimum }), - ], - }); - pipes.push(expression); - } else if (schema.minimum !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.minValue, - }), - parameters: [numberParameter({ isBigInt, value: schema.minimum })], - }); - pipes.push(expression); - } - - if (schema.exclusiveMaximum !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.ltValue, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMaximum }), - ], - }); - pipes.push(expression); - } else if (schema.maximum !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.maxValue, - }), - parameters: [numberParameter({ isBigInt, value: schema.maximum })], - }); - pipes.push(expression); - } - - return pipesToExpression({ pipes, plugin }); -}; - -const objectTypeToValibotSchema = ({ - _path, - plugin, - schema, - state, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'object'>; - state: State; -}): { - anyType: string; - expression: ts.CallExpression; -} => { - // TODO: parser - handle constants - const properties: Array = []; - - const required = schema.required ?? []; - - for (const name in schema.properties) { - const property = schema.properties[name]!; - const isRequired = required.includes(name); - - const schemaPipes = schemaToValibotSchema({ - _path: [..._path, 'properties', name], - optional: !isRequired, - plugin, - schema: property, - state, - }); - - numberRegExp.lastIndex = 0; - let propertyName; - if (numberRegExp.test(name)) { - // For numeric literals, we'll handle negative numbers by using a string literal - // instead of trying to use a PrefixUnaryExpression - propertyName = name.startsWith('-') - ? ts.factory.createStringLiteral(name) - : ts.factory.createNumericLiteral(name); - } else { - propertyName = 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( - tsc.propertyAssignment({ - initializer: pipesToExpression({ pipes: schemaPipes, plugin }), - name: propertyName, - }), - ); - } - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - if ( - schema.additionalProperties && - schema.additionalProperties.type === 'object' && - !Object.keys(properties).length - ) { - const pipes = schemaToValibotSchema({ - _path: [..._path, 'additionalProperties'], - plugin, - schema: schema.additionalProperties, - state, - }); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.record, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.string, - }), - parameters: [], - }), - pipesToExpression({ pipes, plugin }), - ], - }); - return { - anyType: 'AnyZodObject', - expression, - }; - } - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.object, - }), - parameters: [ts.factory.createObjectLiteralExpression(properties, true)], - }); - return { - // Zod uses AnyZodObject here, maybe we want to be more specific too - anyType: identifiers.types.GenericSchema.text, - expression, - }; -}; - -const stringTypeToValibotSchema = ({ - plugin, - schema, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'string'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - if (typeof schema.const === 'string') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); - return expression; - } - - const pipes: Array = []; - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.string, - }), - }); - pipes.push(expression); - - if (schema.format) { - switch (schema.format) { - case 'date': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.isoDate, - }), - }), - ); - break; - case 'date-time': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.isoTimestamp, - }), - }), - ); - break; - case 'ipv4': - case 'ipv6': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.ip, - }), - }), - ); - break; - case 'uri': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.url, - }), - }), - ); - break; - case 'email': - case 'time': - case 'uuid': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: tsc.identifier({ text: schema.format }), - }), - }), - ); - break; - } - } - - if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - pipes.push(expression); - } else { - if (schema.minLength !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.minLength, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - pipes.push(expression); - } - - if (schema.maxLength !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.maxLength, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }); - pipes.push(expression); - } - } - - if (schema.pattern) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }); - pipes.push(expression); - } - - return pipesToExpression({ pipes, plugin }); -}; - -const tupleTypeToValibotSchema = ({ - _path, - plugin, - schema, - state, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'tuple'>; - state: State; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - if (schema.const && Array.isArray(schema.const)) { - const tupleElements = schema.const.map((value) => - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.literal, - }), - parameters: [tsc.valueToExpression({ value })], - }), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return expression; - } - - if (schema.items) { - const tupleElements = schema.items.map((item, index) => { - const schemaPipes = schemaToValibotSchema({ - _path: [..._path, 'items', index], - plugin, - schema: item, - state, - }); - return pipesToExpression({ pipes: schemaPipes, plugin }); - }); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return expression; - } - - return unknownTypeToValibotSchema({ - _path, - plugin, - schema: { - type: 'unknown', - }, - }); -}; - -const undefinedTypeToValibotSchema = ({ - plugin, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'undefined'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.undefined, - }), - }); - return expression; -}; - -const unknownTypeToValibotSchema = ({ - plugin, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'unknown'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.unknown, - }), - }); - return expression; -}; - -const voidTypeToValibotSchema = ({ - plugin, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'void'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.void, - }), - }); - return expression; -}; - -const schemaTypeToValibotSchema = ({ - _path, - plugin, - schema, - state, -}: SchemaToValibotSchemaOptions & { - schema: IR.SchemaObject; - state: State; -}): { - anyType?: string; - expression: ts.Expression; -} => { - switch (schema.type as Required['type']) { - case 'array': - return { - expression: arrayTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'array'>, - state, - }), - }; - case 'boolean': - return { - expression: booleanTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'boolean'>, - }), - }; - case 'enum': - return { - expression: enumTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'enum'>, - }), - }; - case 'integer': - case 'number': - return { - expression: numberTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'integer' | 'number'>, - }), - }; - case 'never': - return { - expression: neverTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'never'>, - }), - }; - case 'null': - return { - expression: nullTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'null'>, - }), - }; - case 'object': - return objectTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'object'>, - state, - }); - case 'string': - // For string schemas with int64/uint64 formats, use number handler to generate union with transform - if (schema.format === 'int64' || schema.format === 'uint64') { - return { - expression: numberTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'integer' | 'number'>, - }), - }; - } - return { - expression: stringTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'string'>, - }), - }; - case 'tuple': - return { - expression: tupleTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'tuple'>, - state, - }), - }; - case 'undefined': - return { - expression: undefinedTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'undefined'>, - }), - }; - case 'unknown': - return { - expression: unknownTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'unknown'>, - }), - }; - case 'void': - return { - expression: voidTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'void'>, - }), - }; - } -}; - -export const schemaToValibotSchema = ({ - $ref, - _path = [], - optional, - plugin, - schema, - state, - symbol, -}: SchemaToValibotSchemaOptions & { - /** - * When $ref is supplied, a node will be emitted to the file. - */ - $ref?: string; - /** - * Accept `optional` to handle optional object properties. We can't handle - * this inside the object function because `.optional()` must come before - * `.default()` which is handled in this function. - */ - optional?: boolean; - schema: IR.SchemaObject; - state: State; - symbol?: Symbol; -}): Array => { - let anyType: string | undefined; - let pipes: Array = []; - - if ($ref) { - state.circularReferenceTracker.add($ref); - - if (!symbol) { - const selector = plugin.api.getSelector('ref', $ref); - if (!plugin.getSymbol(selector)) { - symbol = plugin.referenceSymbol(selector); - } - } - } - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - if (schema.$ref) { - const isCircularReference = state.circularReferenceTracker.has(schema.$ref); - - // if $ref hasn't been processed yet, inline it to avoid the - // "Block-scoped variable used before its declaration." error - // this could be (maybe?) fixed by reshuffling the generation order - const selector = plugin.api.getSelector('ref', schema.$ref); - let refSymbol = plugin.getSymbol(selector); - if (!refSymbol) { - const ref = plugin.context.resolveIrRef(schema.$ref); - const schemaPipes = schemaToValibotSchema({ - $ref: schema.$ref, - _path: jsonPointerToPath(schema.$ref), - plugin, - schema: ref, - state, - }); - pipes.push(...schemaPipes); - - refSymbol = plugin.getSymbol(selector); - } - - if (refSymbol) { - const refIdentifier = tsc.identifier({ text: refSymbol.placeholder }); - if (isCircularReference) { - const lazyExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.lazy, - }), - parameters: [ - tsc.arrowFunction({ - statements: [ - tsc.returnStatement({ - expression: refIdentifier, - }), - ], - }), - ], - }); - pipes.push(lazyExpression); - state.hasCircularReference = true; - } else { - pipes.push(refIdentifier); - } - } - } else if (schema.type) { - const valibotSchema = schemaTypeToValibotSchema({ - _path, - plugin, - schema, - state, - }); - anyType = valibotSchema.anyType; - pipes.push(valibotSchema.expression); - - if (plugin.config.metadata && schema.description) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.metadata, - }), - parameters: [ - tsc.objectExpression({ - obj: [ - { - key: 'description', - value: tsc.stringLiteral({ text: schema.description }), - }, - ], - }), - ], - }); - pipes.push(expression); - } - } else if (schema.items) { - schema = deduplicateSchema({ schema }); - - if (schema.items) { - const itemTypes = schema.items.map((item, index) => { - const schemaPipes = schemaToValibotSchema({ - _path: [..._path, 'items', index], - plugin, - schema: item, - state, - }); - return pipesToExpression({ pipes: schemaPipes, plugin }); - }); - - if (schema.logicalOperator === 'and') { - const intersectExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.intersect, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemTypes, - }), - ], - }); - pipes.push(intersectExpression); - } else { - const unionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemTypes, - }), - ], - }); - pipes.push(unionExpression); - } - } else { - const schemaPipes = schemaToValibotSchema({ - _path, - plugin, - schema, - state, - }); - pipes.push(...schemaPipes); - } - } else { - // catch-all fallback for failed schemas - const valibotSchema = schemaTypeToValibotSchema({ - _path, - plugin, - schema: { - type: 'unknown', - }, - state, - }); - anyType = valibotSchema.anyType; - pipes.push(valibotSchema.expression); - } - - if ($ref) { - state.circularReferenceTracker.delete($ref); - } - - if (pipes.length) { - if (schema.accessScope === 'read') { - const readonlyExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.readonly, - }), - }); - pipes.push(readonlyExpression); - } - } - - if (pipes.length) { - let callParameter: ts.Expression | undefined; - - if (schema.default !== undefined) { - const isBigInt = schema.type === 'integer' && schema.format === 'int64'; - callParameter = numberParameter({ isBigInt, value: schema.default }); - if (callParameter) { - pipes = [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.optional, - }), - parameters: [pipesToExpression({ pipes, plugin }), callParameter], - }), - ]; - } - } - - if (optional && !callParameter) { - pipes = [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.optional, - }), - parameters: [pipesToExpression({ pipes, plugin })], - }), - ]; - } - } - - if (symbol) { - if ($ref) { - symbol = plugin.registerSymbol({ - exported: true, - meta: { - resourceType: pathToSymbolResourceType(_path), - }, - name: buildName({ - config: { - case: state.nameCase, - name: state.nameTransformer, - }, - name: refToName($ref), - }), - selector: plugin.api.getSelector('ref', $ref), - }); - } - const statement = tsc.constVariable({ - comment: plugin.config.comments - ? createSchemaComment({ schema }) - : undefined, - exportConst: symbol.exported, - expression: pipesToExpression({ pipes, plugin }), - name: symbol.placeholder, - typeName: state.hasCircularReference - ? (tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: anyType || identifiers.types.GenericSchema.text, - }) as unknown as ts.TypeNode) - : undefined, - }); - plugin.setSymbolValue(symbol, statement); - return []; - } - - return pipes; -}; - -export const handler: ValibotPlugin['Handler'] = ({ plugin }) => { - plugin.registerSymbol({ - external: 'valibot', - meta: { importKind: 'namespace' }, - name: 'v', - selector: plugin.api.getSelector('import', 'valibot'), - }); - - plugin.forEach( - 'operation', - 'parameter', - 'requestBody', - 'schema', - 'webhook', - (event) => { - const state: State = { - circularReferenceTracker: new Set(), - hasCircularReference: false, - nameCase: plugin.config.definitions.case, - nameTransformer: plugin.config.definitions.name, - }; +import { handlerV1 } from './v1/plugin'; - switch (event.type) { - case 'operation': - operationToValibotSchema({ - _path: event._path, - operation: event.operation, - plugin, - state, - }); - break; - case 'parameter': - schemaToValibotSchema({ - $ref: event.$ref, - _path: event._path, - plugin, - schema: event.parameter.schema, - state, - }); - break; - case 'requestBody': - schemaToValibotSchema({ - $ref: event.$ref, - _path: event._path, - plugin, - schema: event.requestBody.schema, - state, - }); - break; - case 'schema': - schemaToValibotSchema({ - $ref: event.$ref, - _path: event._path, - plugin, - schema: event.schema, - state, - }); - break; - case 'webhook': - webhookToValibotSchema({ - _path: event._path, - operation: event.operation, - plugin, - state, - }); - break; - } - }, - ); -}; +export const handler: ValibotPlugin['Handler'] = (args) => handlerV1(args); diff --git a/packages/openapi-ts/src/plugins/valibot/number-helpers.ts b/packages/openapi-ts/src/plugins/valibot/shared/numbers.ts similarity index 97% rename from packages/openapi-ts/src/plugins/valibot/number-helpers.ts rename to packages/openapi-ts/src/plugins/valibot/shared/numbers.ts index 8fa532a72f..2606f1adae 100644 --- a/packages/openapi-ts/src/plugins/valibot/number-helpers.ts +++ b/packages/openapi-ts/src/plugins/valibot/shared/numbers.ts @@ -1,6 +1,5 @@ -import { tsc } from '../../tsc'; +import { tsc } from '../../../tsc'; -// Integer format ranges and properties export const INTEGER_FORMATS = { int16: { max: 32767, diff --git a/packages/openapi-ts/src/plugins/valibot/shared/types.d.ts b/packages/openapi-ts/src/plugins/valibot/shared/types.d.ts new file mode 100644 index 0000000000..0c4bfeb31e --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/shared/types.d.ts @@ -0,0 +1,24 @@ +import type { IR } from '../../../ir/types'; +import type { StringCase, StringName } from '../../../types/case'; +import type { ToRefs } from '../../shared/types/refs'; +import type { ValibotPlugin } from '../types'; + +export type IrSchemaToAstOptions = { + plugin: ValibotPlugin['Instance']; + state: ToRefs; +}; + +export type PluginState = { + /** + * Path to the schema in the intermediary representation. + */ + _path: ReadonlyArray; + hasLazyExpression: boolean; + nameCase: StringCase; + nameTransformer: StringName; +}; + +export type ValidatorArgs = { + operation: IR.OperationObject; + plugin: ValibotPlugin['Instance']; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/api.ts b/packages/openapi-ts/src/plugins/valibot/v1/api.ts new file mode 100644 index 0000000000..790cfbc0ba --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/api.ts @@ -0,0 +1,85 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../tsc'; +import type { ValidatorArgs } from '../shared/types'; +import { identifiers } from './constants'; + +export const createRequestValidatorV1 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol(plugin.api.selector('data', operation.id)); + if (!symbol) return; + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.async.parseAsync, + }), + parameters: [ + tsc.identifier({ text: symbol.placeholder }), + tsc.identifier({ text: dataParameterName }), + ], + }), + }), + }), + ], + }); +}; + +export const createResponseValidatorV1 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol( + plugin.api.selector('responses', operation.id), + ); + if (!symbol) return; + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.async.parseAsync, + }), + parameters: [ + tsc.identifier({ text: symbol.placeholder }), + tsc.identifier({ text: dataParameterName }), + ], + }), + }), + }), + ], + }); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/constants.ts b/packages/openapi-ts/src/plugins/valibot/v1/constants.ts similarity index 99% rename from packages/openapi-ts/src/plugins/valibot/constants.ts rename to packages/openapi-ts/src/plugins/valibot/v1/constants.ts index 5f1790715a..b3a085d68b 100644 --- a/packages/openapi-ts/src/plugins/valibot/constants.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/constants.ts @@ -1,4 +1,4 @@ -import { tsc } from '../../tsc'; +import { tsc } from '../../../tsc'; export const identifiers = { /** diff --git a/packages/openapi-ts/src/plugins/valibot/operation.ts b/packages/openapi-ts/src/plugins/valibot/v1/operation.ts similarity index 80% rename from packages/openapi-ts/src/plugins/valibot/operation.ts rename to packages/openapi-ts/src/plugins/valibot/v1/operation.ts index b3e9cfd2e9..6e2ddea5b2 100644 --- a/packages/openapi-ts/src/plugins/valibot/operation.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/operation.ts @@ -1,20 +1,17 @@ -import { operationResponsesMap } from '../../ir/operation'; -import type { IR } from '../../ir/types'; -import { buildName } from '../../openApi/shared/utils/name'; -import { pathToSymbolResourceType } from '../shared/utils/meta'; -import { schemaToValibotSchema, type State } from './plugin'; -import type { ValibotPlugin } from './types'; - -export const operationToValibotSchema = ({ - _path, +import { operationResponsesMap } from '../../../ir/operation'; +import type { IR } from '../../../ir/types'; +import { buildName } from '../../../openApi/shared/utils/name'; +import { pathToSymbolResourceType } from '../../shared/utils/meta'; +import { toRef } from '../../shared/utils/refs'; +import type { IrSchemaToAstOptions } from '../shared/types'; +import { irSchemaToAst } from './plugin'; + +export const irOperationToAst = ({ operation, plugin, state, -}: { - _path: ReadonlyArray; +}: IrSchemaToAstOptions & { operation: IR.OperationObject; - plugin: ValibotPlugin['Instance']; - state: State; }) => { if (plugin.config.requests.enabled) { const requiredProperties = new Set(); @@ -117,16 +114,15 @@ export const operationToValibotSchema = ({ const symbol = plugin.registerSymbol({ exported: true, meta: { - resourceType: pathToSymbolResourceType(_path), + resourceType: pathToSymbolResourceType(state._path.value), }, name: buildName({ config: plugin.config.requests, name: operation.id, }), - selector: plugin.api.getSelector('data', operation.id), + selector: plugin.api.selector('data', operation.id), }); - schemaToValibotSchema({ - _path, + irSchemaToAst({ plugin, schema: schemaData, state, @@ -139,7 +135,7 @@ export const operationToValibotSchema = ({ const { response } = operationResponsesMap(operation); if (response) { - const path = [..._path, 'responses']; + const path = [...state._path.value, 'responses']; const symbol = plugin.registerSymbol({ exported: true, meta: { @@ -149,13 +145,15 @@ export const operationToValibotSchema = ({ config: plugin.config.responses, name: operation.id, }), - selector: plugin.api.getSelector('responses', operation.id), + selector: plugin.api.selector('responses', operation.id), }); - schemaToValibotSchema({ - _path: path, + irSchemaToAst({ plugin, schema: response, - state, + state: { + ...state, + _path: toRef(path), + }, symbol, }); } diff --git a/packages/openapi-ts/src/plugins/valibot/v1/pipesToAst.ts b/packages/openapi-ts/src/plugins/valibot/v1/pipesToAst.ts new file mode 100644 index 0000000000..053e8df071 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/pipesToAst.ts @@ -0,0 +1,29 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../tsc'; +import type { ValibotPlugin } from '../types'; +import { identifiers } from './constants'; + +export const pipesToAst = ({ + pipes, + plugin, +}: { + pipes: Array; + plugin: ValibotPlugin['Instance']; +}): ts.Expression => { + if (pipes.length === 1) { + return pipes[0]!; + } + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.methods.pipe, + }), + parameters: pipes, + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/plugin.ts b/packages/openapi-ts/src/plugins/valibot/v1/plugin.ts new file mode 100644 index 0000000000..788af9e917 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/plugin.ts @@ -0,0 +1,314 @@ +import type { Symbol } from '@hey-api/codegen-core'; +import type ts from 'typescript'; + +import { deduplicateSchema } from '../../../ir/schema'; +import type { IR } from '../../../ir/types'; +import { buildName } from '../../../openApi/shared/utils/name'; +import { tsc } from '../../../tsc'; +import { refToName } from '../../../utils/ref'; +import type { SchemaWithType } from '../../shared/types/schema'; +import { pathToSymbolResourceType } from '../../shared/utils/meta'; +import { toRef, toRefs } from '../../shared/utils/refs'; +import { createSchemaComment } from '../../shared/utils/schema'; +import { numberParameter } from '../shared/numbers'; +import type { IrSchemaToAstOptions, PluginState } from '../shared/types'; +import type { ValibotPlugin } from '../types'; +import { identifiers } from './constants'; +import { irOperationToAst } from './operation'; +import { pipesToAst } from './pipesToAst'; +import { irSchemaWithTypeToAst } from './toAst'; +import { irWebhookToAst } from './webhook'; + +export const irSchemaToAst = ({ + $ref, + optional, + plugin, + schema, + state, + symbol, +}: IrSchemaToAstOptions & { + /** + * When $ref is supplied, a node will be emitted to the file. + */ + $ref?: string; + /** + * Accept `optional` to handle optional object properties. We can't handle + * this inside the object function because `.optional()` must come before + * `.default()` which is handled in this function. + */ + optional?: boolean; + schema: IR.SchemaObject; + /** + * When symbol is supplied, the AST node will be set as its value. + */ + symbol?: Symbol; +}): Array => { + if ($ref && !symbol) { + const selector = plugin.api.selector('ref', $ref); + symbol = plugin.getSymbol(selector) || plugin.referenceSymbol(selector); + } + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + let anyType: string | undefined; + let pipes: Array = []; + + if (schema.$ref) { + const selector = plugin.api.selector('ref', schema.$ref); + const refSymbol = plugin.referenceSymbol(selector); + if (plugin.isSymbolRegistered(selector)) { + const ref = tsc.identifier({ text: refSymbol.placeholder }); + pipes.push(ref); + } else { + const lazyExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.lazy, + }), + parameters: [ + tsc.arrowFunction({ + statements: [ + tsc.returnStatement({ + expression: tsc.identifier({ text: refSymbol.placeholder }), + }), + ], + }), + ], + }); + pipes.push(lazyExpression); + state.hasLazyExpression.value = true; + } + } else if (schema.type) { + const ast = irSchemaWithTypeToAst({ + plugin, + schema: schema as SchemaWithType, + state, + }); + anyType = ast.anyType; + pipes.push(ast.expression); + + if (plugin.config.metadata && schema.description) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.metadata, + }), + parameters: [ + tsc.objectExpression({ + obj: [ + { + key: 'description', + value: tsc.stringLiteral({ text: schema.description }), + }, + ], + }), + ], + }); + pipes.push(expression); + } + } else if (schema.items) { + schema = deduplicateSchema({ schema }); + + if (schema.items) { + const itemsAst = schema.items.map((item, index) => { + const itemAst = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + return pipesToAst({ pipes: itemAst, plugin }); + }); + + if (schema.logicalOperator === 'and') { + const intersectExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.intersect, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemsAst, + }), + ], + }); + pipes.push(intersectExpression); + } else { + const unionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemsAst, + }), + ], + }); + pipes.push(unionExpression); + } + } else { + const schemaPipes = irSchemaToAst({ plugin, schema, state }); + pipes.push(...schemaPipes); + } + } else { + // catch-all fallback for failed schemas + const ast = irSchemaWithTypeToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + anyType = ast.anyType; + pipes.push(ast.expression); + } + + if (pipes.length) { + if (schema.accessScope === 'read') { + const readonlyExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.readonly, + }), + }); + pipes.push(readonlyExpression); + } + + let callParameter: ts.Expression | undefined; + + if (schema.default !== undefined) { + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; + callParameter = numberParameter({ isBigInt, value: schema.default }); + if (callParameter) { + pipes = [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.optional, + }), + parameters: [pipesToAst({ pipes, plugin }), callParameter], + }), + ]; + } + } + + if (optional && !callParameter) { + pipes = [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.optional, + }), + parameters: [pipesToAst({ pipes, plugin })], + }), + ]; + } + } + + if (symbol) { + if ($ref) { + symbol = plugin.registerSymbol({ + exported: true, + meta: { + resourceType: pathToSymbolResourceType(state._path.value), + }, + name: buildName({ + config: { + case: state.nameCase.value, + name: state.nameTransformer.value, + }, + name: refToName($ref), + }), + selector: plugin.api.selector('ref', $ref), + }); + } + const statement = tsc.constVariable({ + comment: plugin.config.comments + ? createSchemaComment({ schema }) + : undefined, + exportConst: symbol.exported, + expression: pipesToAst({ pipes, plugin }), + name: symbol.placeholder, + typeName: state.hasLazyExpression.value + ? (tsc.propertyAccessExpression({ + expression: v.placeholder, + name: anyType || identifiers.types.GenericSchema.text, + }) as unknown as ts.TypeNode) + : undefined, + }); + plugin.setSymbolValue(symbol, statement); + return []; + } + + return pipes; +}; + +export const handlerV1: ValibotPlugin['Handler'] = ({ plugin }) => { + plugin.registerSymbol({ + external: 'valibot', + meta: { importKind: 'namespace' }, + name: 'v', + selector: plugin.api.selector('external', 'valibot.v'), + }); + + plugin.forEach( + 'operation', + 'parameter', + 'requestBody', + 'schema', + 'webhook', + (event) => { + const state = toRefs({ + _path: event._path, + hasLazyExpression: false, + nameCase: plugin.config.definitions.case, + nameTransformer: plugin.config.definitions.name, + }); + + switch (event.type) { + case 'operation': + irOperationToAst({ + operation: event.operation, + plugin, + state, + }); + break; + case 'parameter': + irSchemaToAst({ + $ref: event.$ref, + plugin, + schema: event.parameter.schema, + state, + }); + break; + case 'requestBody': + irSchemaToAst({ + $ref: event.$ref, + plugin, + schema: event.requestBody.schema, + state, + }); + break; + case 'schema': + irSchemaToAst({ + $ref: event.$ref, + plugin, + schema: event.schema, + state, + }); + break; + case 'webhook': + irWebhookToAst({ + operation: event.operation, + plugin, + state, + }); + break; + } + }, + ); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts new file mode 100644 index 0000000000..9bd2fb2068 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts @@ -0,0 +1,127 @@ +import type ts from 'typescript'; + +import { deduplicateSchema } from '../../../../ir/schema'; +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { pipesToAst } from '../pipesToAst'; +import { irSchemaToAst } from '../plugin'; +import { unknownToAst } from './unknown'; + +export const arrayToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'array'>; +}): ts.Expression => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + const functionName = tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.array, + }); + + const pipes: Array = []; + + if (!schema.items) { + const expression = tsc.callExpression({ + functionName, + parameters: [ + unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }), + ], + }); + pipes.push(expression); + } else { + schema = deduplicateSchema({ schema }); + + // at least one item is guaranteed + const itemExpressions = schema.items!.map((item, index) => { + const schemaPipes = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + return pipesToAst({ pipes: schemaPipes, plugin }); + }); + + if (itemExpressions.length === 1) { + const expression = tsc.callExpression({ + functionName, + parameters: itemExpressions, + }); + pipes.push(expression); + } else { + if (schema.logicalOperator === 'and') { + // TODO: parser - handle intersection + // return tsc.typeArrayNode( + // tsc.typeIntersectionNode({ types: itemExpressions }), + // ); + } + + // TODO: parser - handle union + // return tsc.typeArrayNode(tsc.typeUnionNode({ types: itemExpressions })); + + const expression = tsc.callExpression({ + functionName, + parameters: [ + unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }), + ], + }); + pipes.push(expression); + } + } + + if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + pipes.push(expression); + } else { + if (schema.minItems !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.minLength, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + pipes.push(expression); + } + + if (schema.maxItems !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.maxLength, + }), + parameters: [tsc.valueToExpression({ value: schema.maxItems })], + }); + pipes.push(expression); + } + } + + return pipesToAst({ pipes, plugin }); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts new file mode 100644 index 0000000000..149f11be9c --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts @@ -0,0 +1,34 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const booleanToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'boolean'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + if (typeof schema.const === 'boolean') { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.literal, + }), + parameters: [tsc.ots.boolean(schema.const)], + }); + return expression; + } + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.boolean, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts new file mode 100644 index 0000000000..dd139d9903 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts @@ -0,0 +1,71 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { unknownToAst } from './unknown'; + +export const enumToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'enum'>; +}): ts.CallExpression => { + const enumMembers: Array = []; + + let isNullable = false; + + for (const item of schema.items ?? []) { + // Zod supports only string enums + if (item.type === 'string' && typeof item.const === 'string') { + enumMembers.push( + tsc.stringLiteral({ + text: item.const, + }), + ); + } else if (item.type === 'null' || item.const === null) { + isNullable = true; + } + } + + if (!enumMembers.length) { + return unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + } + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + let resultExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.picklist, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: enumMembers, + multiLine: false, + }), + ], + }); + + if (isNullable) { + resultExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.nullable, + }), + parameters: [resultExpression], + }); + } + + return resultExpression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts new file mode 100644 index 0000000000..4750ebcc6e --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts @@ -0,0 +1,121 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { arrayToAst } from './array'; +import { booleanToAst } from './boolean'; +import { enumToAst } from './enum'; +import { neverToAst } from './never'; +import { nullToAst } from './null'; +import { numberToAst } from './number'; +import { objectToAst } from './object'; +import { stringToAst } from './string'; +import { tupleToAst } from './tuple'; +import { undefinedToAst } from './undefined'; +import { unknownToAst } from './unknown'; +import { voidToAst } from './void'; + +export const irSchemaWithTypeToAst = ({ + schema, + ...args +}: IrSchemaToAstOptions & { + schema: SchemaWithType; +}): { + anyType?: string; + expression: ts.Expression; +} => { + switch (schema.type) { + case 'array': + return { + expression: arrayToAst({ + ...args, + schema: schema as SchemaWithType<'array'>, + }), + }; + case 'boolean': + return { + expression: booleanToAst({ + ...args, + schema: schema as SchemaWithType<'boolean'>, + }), + }; + case 'enum': + return { + expression: enumToAst({ + ...args, + schema: schema as SchemaWithType<'enum'>, + }), + }; + case 'integer': + case 'number': + return { + expression: numberToAst({ + ...args, + schema: schema as SchemaWithType<'integer' | 'number'>, + }), + }; + case 'never': + return { + expression: neverToAst({ + ...args, + schema: schema as SchemaWithType<'never'>, + }), + }; + case 'null': + return { + expression: nullToAst({ + ...args, + schema: schema as SchemaWithType<'null'>, + }), + }; + case 'object': + return objectToAst({ + ...args, + schema: schema as SchemaWithType<'object'>, + }); + case 'string': + // For string schemas with int64/uint64 formats, use number handler to generate union with transform + if (schema.format === 'int64' || schema.format === 'uint64') { + return { + expression: numberToAst({ + ...args, + schema: schema as SchemaWithType<'integer' | 'number'>, + }), + }; + } + return { + expression: stringToAst({ + ...args, + schema: schema as SchemaWithType<'string'>, + }), + }; + case 'tuple': + return { + expression: tupleToAst({ + ...args, + schema: schema as SchemaWithType<'tuple'>, + }), + }; + case 'undefined': + return { + expression: undefinedToAst({ + ...args, + schema: schema as SchemaWithType<'undefined'>, + }), + }; + case 'unknown': + return { + expression: unknownToAst({ + ...args, + schema: schema as SchemaWithType<'unknown'>, + }), + }; + case 'void': + return { + expression: voidToAst({ + ...args, + schema: schema as SchemaWithType<'void'>, + }), + }; + } +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts new file mode 100644 index 0000000000..06613a6374 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts @@ -0,0 +1,21 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const neverToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'never'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.never, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts new file mode 100644 index 0000000000..281f3e9aeb --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts @@ -0,0 +1,21 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const nullToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'null'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.null, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts new file mode 100644 index 0000000000..e6f0c66e6e --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts @@ -0,0 +1,254 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { + INTEGER_FORMATS, + isIntegerFormat, + needsBigIntForFormat, + numberParameter, +} from '../../shared/numbers'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { pipesToAst } from '../pipesToAst'; + +export const numberToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'integer' | 'number'>; +}) => { + const format = schema.format; + const isInteger = schema.type === 'integer'; + const isBigInt = needsBigIntForFormat(format); + const formatInfo = isIntegerFormat(format) ? INTEGER_FORMATS[format] : null; + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + // Return early if const is defined since we can create a literal type directly without additional validation + if (schema.const !== undefined && schema.const !== null) { + const constValue = schema.const; + let literalValue; + + // Case 1: Number with no format -> generate literal with the number + if (typeof constValue === 'number' && !format) { + literalValue = tsc.ots.number(constValue); + } + // Case 2: Number with format -> check if format needs BigInt, generate appropriate literal + else if (typeof constValue === 'number' && format) { + if (isBigInt) { + // Format requires BigInt, convert number to BigInt + literalValue = tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.ots.string(constValue.toString())], + }); + } else { + // Regular format, use number as-is + literalValue = tsc.ots.number(constValue); + } + } + // Case 3: Format that allows string -> generate BigInt literal (for int64/uint64 formats) + else if (typeof constValue === 'string' && isBigInt) { + // Remove 'n' suffix if present in string + const cleanString = constValue.endsWith('n') + ? constValue.slice(0, -1) + : constValue; + literalValue = tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.ots.string(cleanString)], + }); + } + // Case 4: Const is typeof bigint (literal) -> transform from literal to BigInt() + else if (typeof constValue === 'bigint') { + // Convert BigInt to string and remove 'n' suffix that toString() adds + const bigintString = constValue.toString(); + const cleanString = bigintString.endsWith('n') + ? bigintString.slice(0, -1) + : bigintString; + literalValue = tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.ots.string(cleanString)], + }); + } + // Default case: use value as-is for other types + else { + literalValue = tsc.valueToExpression({ value: constValue }); + } + + return tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.literal, + }), + parameters: [literalValue], + }); + } + + const pipes: Array = []; + + // For bigint formats (int64, uint64), create union of number, string, and bigint with transform + if (isBigInt) { + const unionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.number, + }), + }), + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.string, + }), + }), + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.bigInt, + }), + }), + ], + multiLine: false, + }), + ], + }); + pipes.push(unionExpression); + + // Add transform to convert to BigInt + const transformExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.transform, + }), + parameters: [ + tsc.arrowFunction({ + parameters: [{ name: 'x' }], + statements: tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.identifier({ text: 'x' })], + }), + }), + ], + }); + pipes.push(transformExpression); + } else { + // For regular number formats, use number schema + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.number, + }), + }); + pipes.push(expression); + } + + // Add integer validation for integer types (except when using bigint union) + if (!isBigInt && isInteger) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.integer, + }), + }); + pipes.push(expression); + } + + // Add format-specific range validations + if (formatInfo) { + const minValue = formatInfo.min; + const maxValue = formatInfo.max; + const minErrorMessage = formatInfo.minError; + const maxErrorMessage = formatInfo.maxError; + + // Add minimum value validation + const minExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.minValue, + }), + parameters: [ + isBigInt + ? tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.ots.string(minValue.toString())], + }) + : tsc.ots.number(minValue as number), + tsc.ots.string(minErrorMessage), + ], + }); + pipes.push(minExpression); + + // Add maximum value validation + const maxExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.maxValue, + }), + parameters: [ + isBigInt + ? tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.ots.string(maxValue.toString())], + }) + : tsc.ots.number(maxValue as number), + tsc.ots.string(maxErrorMessage), + ], + }); + pipes.push(maxExpression); + } + + if (schema.exclusiveMinimum !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.gtValue, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMinimum }), + ], + }); + pipes.push(expression); + } else if (schema.minimum !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.minValue, + }), + parameters: [numberParameter({ isBigInt, value: schema.minimum })], + }); + pipes.push(expression); + } + + if (schema.exclusiveMaximum !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.ltValue, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMaximum }), + ], + }); + pipes.push(expression); + } else if (schema.maximum !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.maxValue, + }), + parameters: [numberParameter({ isBigInt, value: schema.maximum })], + }); + pipes.push(expression); + } + + return pipesToAst({ pipes, plugin }); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts new file mode 100644 index 0000000000..b85bec76a7 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts @@ -0,0 +1,119 @@ +import ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import { numberRegExp } from '../../../../utils/regexp'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { pipesToAst } from '../pipesToAst'; +import { irSchemaToAst } from '../plugin'; + +export const objectToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'object'>; +}): { + anyType: string; + expression: ts.CallExpression; +} => { + // TODO: parser - handle constants + const properties: Array = []; + + const required = schema.required ?? []; + + for (const name in schema.properties) { + const property = schema.properties[name]!; + const isRequired = required.includes(name); + + const schemaPipes = irSchemaToAst({ + optional: !isRequired, + plugin, + schema: property, + state: { + ...state, + _path: toRef([...state._path.value, 'properties', name]), + }, + }); + + numberRegExp.lastIndex = 0; + let propertyName; + if (numberRegExp.test(name)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = name.startsWith('-') + ? ts.factory.createStringLiteral(name) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = 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( + tsc.propertyAssignment({ + initializer: pipesToAst({ pipes: schemaPipes, plugin }), + name: propertyName, + }), + ); + } + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + if ( + schema.additionalProperties && + schema.additionalProperties.type === 'object' && + !Object.keys(properties).length + ) { + const pipes = irSchemaToAst({ + plugin, + schema: schema.additionalProperties, + state: { + ...state, + _path: toRef([...state._path.value, 'additionalProperties']), + }, + }); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.record, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.string, + }), + parameters: [], + }), + pipesToAst({ pipes, plugin }), + ], + }); + return { + anyType: 'AnyZodObject', + expression, + }; + } + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.object, + }), + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], + }); + return { + // Zod uses AnyZodObject here, maybe we want to be more specific too + anyType: identifiers.types.GenericSchema.text, + expression, + }; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts new file mode 100644 index 0000000000..e54c6808c8 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts @@ -0,0 +1,143 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { pipesToAst } from '../pipesToAst'; + +export const stringToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'string'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + if (typeof schema.const === 'string') { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.literal, + }), + parameters: [tsc.ots.string(schema.const)], + }); + return expression; + } + + const pipes: Array = []; + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.string, + }), + }); + pipes.push(expression); + + if (schema.format) { + switch (schema.format) { + case 'date': + pipes.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.isoDate, + }), + }), + ); + break; + case 'date-time': + pipes.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.isoTimestamp, + }), + }), + ); + break; + case 'ipv4': + case 'ipv6': + pipes.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.ip, + }), + }), + ); + break; + case 'uri': + pipes.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.url, + }), + }), + ); + break; + case 'email': + case 'time': + case 'uuid': + pipes.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: tsc.identifier({ text: schema.format }), + }), + }), + ); + break; + } + } + + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + pipes.push(expression); + } else { + if (schema.minLength !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.minLength, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + pipes.push(expression); + } + + if (schema.maxLength !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.maxLength, + }), + parameters: [tsc.valueToExpression({ value: schema.maxLength })], + }); + pipes.push(expression); + } + } + + if (schema.pattern) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.regex, + }), + parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], + }); + pipes.push(expression); + } + + return pipesToAst({ pipes, plugin }); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts new file mode 100644 index 0000000000..776103b489 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts @@ -0,0 +1,78 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { pipesToAst } from '../pipesToAst'; +import { irSchemaToAst } from '../plugin'; +import { unknownToAst } from './unknown'; + +export const tupleToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'tuple'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + if (schema.const && Array.isArray(schema.const)) { + const tupleElements = schema.const.map((value) => + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.literal, + }), + parameters: [tsc.valueToExpression({ value })], + }), + ); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return expression; + } + + if (schema.items) { + const tupleElements = schema.items.map((item, index) => { + const schemaPipes = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + return pipesToAst({ pipes: schemaPipes, plugin }); + }); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return expression; + } + + return unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts new file mode 100644 index 0000000000..d4c1167e52 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts @@ -0,0 +1,22 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const undefinedToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'undefined'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.undefined, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts new file mode 100644 index 0000000000..7055154147 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts @@ -0,0 +1,22 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const unknownToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'unknown'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.unknown, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts new file mode 100644 index 0000000000..b92cad06a3 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts @@ -0,0 +1,22 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const voidToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'void'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.void, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/webhook.ts b/packages/openapi-ts/src/plugins/valibot/v1/webhook.ts similarity index 83% rename from packages/openapi-ts/src/plugins/valibot/webhook.ts rename to packages/openapi-ts/src/plugins/valibot/v1/webhook.ts index 6400d46b75..e4f4c3042b 100644 --- a/packages/openapi-ts/src/plugins/valibot/webhook.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/webhook.ts @@ -1,19 +1,15 @@ -import type { IR } from '../../ir/types'; -import { buildName } from '../../openApi/shared/utils/name'; -import { pathToSymbolResourceType } from '../shared/utils/meta'; -import { schemaToValibotSchema, type State } from './plugin'; -import type { ValibotPlugin } from './types'; - -export const webhookToValibotSchema = ({ - _path, +import type { IR } from '../../../ir/types'; +import { buildName } from '../../../openApi/shared/utils/name'; +import { pathToSymbolResourceType } from '../../shared/utils/meta'; +import type { IrSchemaToAstOptions } from '../shared/types'; +import { irSchemaToAst } from './plugin'; + +export const irWebhookToAst = ({ operation, plugin, state, -}: { - _path: ReadonlyArray; +}: IrSchemaToAstOptions & { operation: IR.OperationObject; - plugin: ValibotPlugin['Instance']; - state: State; }) => { if (plugin.config.webhooks.enabled) { const requiredProperties = new Set(); @@ -116,16 +112,15 @@ export const webhookToValibotSchema = ({ const symbol = plugin.registerSymbol({ exported: true, meta: { - resourceType: pathToSymbolResourceType(_path), + resourceType: pathToSymbolResourceType(state._path.value), }, name: buildName({ config: plugin.config.webhooks, name: operation.id, }), - selector: plugin.api.getSelector('webhook-request', operation.id), + selector: plugin.api.selector('webhook-request', operation.id), }); - schemaToValibotSchema({ - _path, + irSchemaToAst({ plugin, schema: schemaData, state, diff --git a/packages/openapi-ts/src/plugins/zod/api.ts b/packages/openapi-ts/src/plugins/zod/api.ts index 0c78dc943e..1fe8d147ec 100644 --- a/packages/openapi-ts/src/plugins/zod/api.ts +++ b/packages/openapi-ts/src/plugins/zod/api.ts @@ -1,15 +1,18 @@ import type { Selector } from '@hey-api/codegen-core'; import type ts from 'typescript'; -import type { IR } from '../../ir/types'; -import { tsc } from '../../tsc'; import type { Plugin } from '../types'; -import { identifiers } from './constants'; -import type { ZodPlugin } from './types'; +import { + createRequestValidatorMini, + createResponseValidatorMini, +} from './mini/api'; +import type { ValidatorArgs } from './shared/types'; +import { createRequestValidatorV3, createResponseValidatorV3 } from './v3/api'; +import { createRequestValidatorV4, createResponseValidatorV4 } from './v4/api'; type SelectorType = | 'data' - | 'import' + | 'external' | 'ref' | 'responses' | 'type-infer-data' @@ -18,11 +21,6 @@ type SelectorType = | 'type-infer-webhook-request' | 'webhook-request'; -type ValidatorArgs = { - operation: IR.OperationObject; - plugin: ZodPlugin['Instance']; -}; - export type IApi = { createRequestValidator: (args: ValidatorArgs) => ts.ArrowFunction | undefined; createResponseValidator: ( @@ -32,7 +30,7 @@ export type IApi = { * @param type Selector type. * @param value Depends on `type`: * - `data`: `operation.id` string - * - `import`: headless symbols representing module imports + * - `external`: external modules * - `ref`: `$ref` JSON pointer * - `responses`: `operation.id` string * - `type-infer-data`: `operation.id` string @@ -42,81 +40,39 @@ export type IApi = { * - `webhook-request`: `operation.id` string * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'zod'>) {} - createRequestValidator({ - operation, - plugin, - }: ValidatorArgs): ts.ArrowFunction | undefined { - const symbol = plugin.getSymbol( - plugin.api.getSelector('data', operation.id), - ); - if (!symbol) return; - - const dataParameterName = 'data'; - - return tsc.arrowFunction({ - async: true, - parameters: [ - { - name: dataParameterName, - }, - ], - statements: [ - tsc.returnStatement({ - expression: tsc.awaitExpression({ - expression: tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: symbol.placeholder, - name: identifiers.parseAsync, - }), - parameters: [tsc.identifier({ text: dataParameterName })], - }), - }), - }), - ], - }); + createRequestValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + const { plugin } = args; + switch (plugin.config.compatibilityVersion) { + case 3: + return createRequestValidatorV3(args); + case 'mini': + return createRequestValidatorMini(args); + case 4: + default: + return createRequestValidatorV4(args); + } } - createResponseValidator({ - operation, - plugin, - }: ValidatorArgs): ts.ArrowFunction | undefined { - const symbol = plugin.getSymbol( - plugin.api.getSelector('responses', operation.id), - ); - if (!symbol) return; - - const dataParameterName = 'data'; - - return tsc.arrowFunction({ - async: true, - parameters: [ - { - name: dataParameterName, - }, - ], - statements: [ - tsc.returnStatement({ - expression: tsc.awaitExpression({ - expression: tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: symbol.placeholder, - name: identifiers.parseAsync, - }), - parameters: [tsc.identifier({ text: dataParameterName })], - }), - }), - }), - ], - }); + createResponseValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + const { plugin } = args; + switch (plugin.config.compatibilityVersion) { + case 3: + return createResponseValidatorV3(args); + case 'mini': + return createResponseValidatorMini(args); + case 4: + default: + return createResponseValidatorV4(args); + } } - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/zod/constants.ts b/packages/openapi-ts/src/plugins/zod/constants.ts index 5fbf81c8e7..d0df73c8db 100644 --- a/packages/openapi-ts/src/plugins/zod/constants.ts +++ b/packages/openapi-ts/src/plugins/zod/constants.ts @@ -1,5 +1,6 @@ import { tsc } from '../../tsc'; +// TODO: this is inaccurate, it combines identifiers for all supported versions export const identifiers = { ZodMiniOptional: tsc.identifier({ text: 'ZodMiniOptional' }), ZodOptional: tsc.identifier({ text: 'ZodOptional' }), diff --git a/packages/openapi-ts/src/plugins/zod/mini/api.ts b/packages/openapi-ts/src/plugins/zod/mini/api.ts new file mode 100644 index 0000000000..f360d67ed1 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/api.ts @@ -0,0 +1,71 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../tsc'; +import { identifiers } from '../constants'; +import type { ValidatorArgs } from '../shared/types'; + +export const createRequestValidatorMini = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol(plugin.api.selector('data', operation.id)); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; + +export const createResponseValidatorMini = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol( + plugin.api.selector('responses', operation.id), + ); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/plugin.ts b/packages/openapi-ts/src/plugins/zod/mini/plugin.ts index 4c03374eb0..ae881579df 100644 --- a/packages/openapi-ts/src/plugins/zod/mini/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/mini/plugin.ts @@ -1,1125 +1,84 @@ -import ts from 'typescript'; - import { deduplicateSchema } from '../../../ir/schema'; import type { IR } from '../../../ir/types'; import { buildName } from '../../../openApi/shared/utils/name'; import { tsc } from '../../../tsc'; import { refToName } from '../../../utils/ref'; -import { numberRegExp } from '../../../utils/regexp'; +import type { SchemaWithType } from '../../shared/types/schema'; +import { pathToSymbolResourceType } from '../../shared/utils/meta'; +import { toRef, toRefs } from '../../shared/utils/refs'; import { identifiers } from '../constants'; -import { exportZodSchema } from '../export'; +import { exportAst } from '../shared/export'; import { getZodModule } from '../shared/module'; -import { operationToZodSchema } from '../shared/operation'; -import type { SchemaWithType, State, ZodSchema } from '../shared/types'; -import { webhookToZodSchema } from '../shared/webhook'; +import { numberParameter } from '../shared/numbers'; +import { irOperationToAst } from '../shared/operation'; +import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; +import { irWebhookToAst } from '../shared/webhook'; import type { ZodPlugin } from '../types'; +import { irSchemaWithTypeToAst } from './toAst'; -const arrayTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'array'>; - state: State; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - const functionName = tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }); - - if (!schema.items) { - result.expression = tsc.callExpression({ - functionName, - parameters: [ - unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }).expression, - ], - }); - } else { - schema = deduplicateSchema({ schema }); - - // at least one item is guaranteed - const itemExpressions = schema.items!.map((item) => { - const zodSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - if (zodSchema.hasCircularReference) { - result.hasCircularReference = true; - } - return zodSchema.expression; - }); - - if (itemExpressions.length === 1) { - result.expression = tsc.callExpression({ - functionName, - parameters: itemExpressions, - }); - } else { - 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 - // `.intersection()` as that does not exist on `.union()` and non-object schemas. - let intersectionExpression: ts.Expression; - if ( - firstSchema.logicalOperator === 'or' || - (firstSchema.type && firstSchema.type !== 'object') - ) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.intersection, - }), - parameters: itemExpressions, - }); - } else { - intersectionExpression = itemExpressions[0]!; - for (let i = 1; i < itemExpressions.length; i++) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.intersection, - }), - parameters: [intersectionExpression, itemExpressions[i]!], - }); - } - } - - result.expression = tsc.callExpression({ - functionName, - parameters: [intersectionExpression], - }); - } else { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemExpressions, - }), - ], - }), - ], - }); - } - } - } - - const checks: Array = []; - - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }), - ); - } else { - if (schema.minItems !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.minLength, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }), - ); - } - - if (schema.maxItems !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.maxLength, - }), - parameters: [tsc.valueToExpression({ value: schema.maxItems })], - }), - ); - } - } - - if (checks.length) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.check, - }), - parameters: checks, - }); - } - - return result as Omit; -}; - -const booleanTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'boolean'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - if (typeof schema.const === 'boolean') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.boolean, - }), - }); - return result as Omit; -}; - -const enumTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'enum'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - const enumMembers: Array = []; - const literalMembers: Array = []; - - let isNullable = false; - let allStrings = true; - - for (const item of schema.items ?? []) { - // Zod supports string, number, and boolean enums - if (item.type === 'string' && typeof item.const === 'string') { - const stringLiteral = tsc.stringLiteral({ - text: item.const, - }); - enumMembers.push(stringLiteral); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [stringLiteral], - }), - ); - } else if ( - (item.type === 'number' || item.type === 'integer') && - typeof item.const === 'number' - ) { - allStrings = false; - const numberLiteral = tsc.ots.number(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [numberLiteral], - }), - ); - } else if (item.type === 'boolean' && typeof item.const === 'boolean') { - allStrings = false; - const booleanLiteral = tsc.ots.boolean(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [booleanLiteral], - }), - ); - } else if (item.type === 'null' || item.const === null) { - isNullable = true; - } - } - - if (!literalMembers.length) { - return unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }); - } - - // Use z.enum() for pure string enums, z.union() for mixed or non-string types - if (allStrings && enumMembers.length > 0) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.enum, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: enumMembers, - multiLine: false, - }), - ], - }); - } else if (literalMembers.length === 1) { - // For single-member unions, use the member directly instead of wrapping in z.union() - result.expression = literalMembers[0]; - } else { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: literalMembers, - multiLine: literalMembers.length > 3, - }), - ], - }); - } - - if (isNullable) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.nullable, - }), - parameters: [result.expression], - }); - } - - return result as Omit; -}; - -const neverTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'never'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const result: Partial> = {}; - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.never, - }), - }); - return result as Omit; -}; - -const nullTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'null'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const result: Partial> = {}; - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.null, - }), - }); - return result as Omit; -}; - -const numberParameter = ({ - isBigInt, - value, -}: { - isBigInt: boolean; - value: unknown; -}): ts.Expression | undefined => { - const expression = tsc.valueToExpression({ value }); - - if ( - isBigInt && - (typeof value === 'bigint' || - typeof value === 'number' || - typeof value === 'string' || - typeof value === 'boolean') - ) { - return tsc.callExpression({ - functionName: 'BigInt', - parameters: [expression], - }); - } - - return expression; -}; - -const numberTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'integer' | 'number'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - const isBigInt = schema.type === 'integer' && schema.format === 'int64'; - - if (typeof schema.const === 'number') { - // TODO: parser - handle bigint constants - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.number(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: isBigInt - ? tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.coerce, - }), - name: identifiers.bigint, - }) - : tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.number, - }), - }); - - if (!isBigInt && schema.type === 'integer') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.int, - }), - }); - } - - const checks: Array = []; - - if (schema.exclusiveMinimum !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.gt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMinimum }), - ], - }), - ); - } else if (schema.minimum !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.gte, - }), - parameters: [numberParameter({ isBigInt, value: schema.minimum })], - }), - ); - } - - if (schema.exclusiveMaximum !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.lt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMaximum }), - ], - }), - ); - } else if (schema.maximum !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.lte, - }), - parameters: [numberParameter({ isBigInt, value: schema.maximum })], - }), - ); - } - - if (checks.length) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.check, - }), - parameters: checks, - }); - } - - return result as Omit; -}; - -const objectTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'object'>; - state: State; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - // TODO: parser - handle constants - const properties: Array = - []; - - const required = schema.required ?? []; - - for (const name in schema.properties) { - const property = schema.properties[name]!; - const isRequired = required.includes(name); - - const propertySchema = schemaToZodSchema({ - optional: !isRequired, - plugin, - schema: property, - state, - }); - if (propertySchema.hasCircularReference) { - result.hasCircularReference = true; - } - - numberRegExp.lastIndex = 0; - let propertyName; - if (numberRegExp.test(name)) { - // For numeric literals, we'll handle negative numbers by using a string literal - // instead of trying to use a PrefixUnaryExpression - propertyName = name.startsWith('-') - ? ts.factory.createStringLiteral(name) - : ts.factory.createNumericLiteral(name); - } else { - propertyName = 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}'`; - } - - if (propertySchema.hasCircularReference) { - properties.push( - tsc.getAccessorDeclaration({ - name: propertyName, - // @ts-expect-error - returnType: propertySchema.typeName - ? tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: propertySchema.typeName, - }) - : undefined, - statements: [ - tsc.returnStatement({ - expression: propertySchema.expression, - }), - ], - }), - ); - } else { - properties.push( - tsc.propertyAssignment({ - initializer: propertySchema.expression, - name: propertyName, - }), - ); - } - } - - if ( - schema.additionalProperties && - (!schema.properties || !Object.keys(schema.properties).length) - ) { - const zodSchema = schemaToZodSchema({ - plugin, - schema: schema.additionalProperties, - state, - }); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.record, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.string, - }), - parameters: [], - }), - zodSchema.expression, - ], - }); - if (zodSchema.hasCircularReference) { - result.hasCircularReference = true; - } - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.object, - }), - parameters: [ts.factory.createObjectLiteralExpression(properties, true)], - }); - - return result as Omit; -}; - -const stringTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'string'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - if (typeof schema.const === 'string') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.string, - }), - }); - - const dateTimeOptions: { key: string; value: boolean }[] = []; - - if (plugin.config.dates.offset) { - dateTimeOptions.push({ key: 'offset', value: true }); - } - if (plugin.config.dates.local) { - dateTimeOptions.push({ key: 'local', value: true }); - } - - if (schema.format) { - switch (schema.format) { - case 'date': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.date, - }), - }); - break; - case 'date-time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.datetime, - }), - parameters: - dateTimeOptions.length > 0 - ? [ - tsc.objectExpression({ - obj: dateTimeOptions, - }), - ] - : [], - }); - break; - case 'email': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.email, - }), - }); - break; - case 'ipv4': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.ipv4, - }), - }); - break; - case 'ipv6': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.ipv6, - }), - }); - break; - case 'time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.time, - }), - }); - break; - case 'uri': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.url, - }), - }); - break; - case 'uuid': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.uuid, - }), - }); - break; - } - } - - const checks: Array = []; - - if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }), - ); - } else { - if (schema.minLength !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.minLength, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }), - ); - } - - if (schema.maxLength !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.maxLength, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }), - ); - } - } - - if (schema.pattern) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }), - ); - } - - if (checks.length) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.check, - }), - parameters: checks, - }); - } - - return result as Omit; -}; - -const tupleTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'tuple'>; - state: State; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - if (schema.const && Array.isArray(schema.const)) { - const tupleElements = schema.const.map((value) => - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.valueToExpression({ value })], - }), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return result as Omit; - } - - const tupleElements: Array = []; - - for (const item of schema.items ?? []) { - const itemSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - tupleElements.push(itemSchema.expression); - - if (itemSchema.hasCircularReference) { - result.hasCircularReference = true; - } - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - - return result as Omit; -}; - -const undefinedTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'undefined'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const result: Partial> = {}; - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.undefined, - }), - }); - return result as Omit; -}; - -const unknownTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'unknown'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const result: Partial> = {}; - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.unknown, - }), - }); - return result as Omit; -}; - -const voidTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'void'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const result: Partial> = {}; - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.void, - }), - }); - return result as Omit; -}; - -const schemaTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: IR.SchemaObject; - state: State; -}): Omit => { - switch (schema.type as Required['type']) { - case 'array': - return arrayTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'array'>, - state, - }); - case 'boolean': - return booleanTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'boolean'>, - }); - case 'enum': - return enumTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'enum'>, - }); - case 'integer': - case 'number': - return numberTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'integer' | 'number'>, - }); - case 'never': - return neverTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'never'>, - }); - case 'null': - return nullTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'null'>, - }); - case 'object': - return objectTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'object'>, - state, - }); - case 'string': - return stringTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'string'>, - }); - case 'tuple': - return tupleTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'tuple'>, - state, - }); - case 'undefined': - return undefinedTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'undefined'>, - }); - case 'unknown': - return unknownTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'unknown'>, - }); - case 'void': - return voidTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'void'>, - }); - } -}; - -const schemaToZodSchema = ({ +export const irSchemaToAst = ({ optional, plugin, schema, state, -}: { +}: IrSchemaToAstOptions & { /** * Accept `optional` to handle optional object properties. We can't handle * this inside the object function because `.optional()` must come before * `.default()` which is handled in this function. */ optional?: boolean; - plugin: ZodPlugin['Instance']; schema: IR.SchemaObject; - state: State; -}): ZodSchema => { - let zodSchema: Partial = {}; +}): Ast => { + let ast: Partial = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); if (schema.$ref) { - const isCircularReference = state.circularReferenceTracker.includes( - schema.$ref, - ); - const isSelfReference = state.currentReferenceTracker.includes(schema.$ref); - state.circularReferenceTracker.push(schema.$ref); - state.currentReferenceTracker.push(schema.$ref); - - const selector = plugin.api.getSelector('ref', schema.$ref); - let symbol = plugin.getSymbol(selector); - - if (isCircularReference) { - if (!symbol) { - symbol = plugin.referenceSymbol(selector); - } - - if (isSelfReference) { - zodSchema.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.lazy, - }), - parameters: [ - tsc.arrowFunction({ - returnType: tsc.keywordTypeNode({ keyword: 'any' }), - statements: [ - tsc.returnStatement({ - expression: tsc.identifier({ text: symbol.placeholder }), - }), - ], - }), - ], - }); - } else { - zodSchema.expression = tsc.identifier({ text: symbol.placeholder }); - } - zodSchema.hasCircularReference = schema.circular; + const selector = plugin.api.selector('ref', schema.$ref); + const refSymbol = plugin.referenceSymbol(selector); + if (plugin.isSymbolRegistered(selector)) { + const ref = tsc.identifier({ text: refSymbol.placeholder }); + ast.expression = ref; } else { - if (!symbol) { - // if $ref hasn't been processed yet, inline it to avoid the - // "Block-scoped variable used before its declaration." error - // this could be (maybe?) fixed by reshuffling the generation order - const ref = plugin.context.resolveIrRef(schema.$ref); - handleComponent({ - id: schema.$ref, - plugin, - schema: ref, - state, - }); - } else { - zodSchema.hasCircularReference = schema.circular; - } - - const refSymbol = plugin.referenceSymbol(selector); - zodSchema.expression = tsc.identifier({ text: refSymbol.placeholder }); + const lazyExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.lazy, + }), + parameters: [ + tsc.arrowFunction({ + returnType: tsc.keywordTypeNode({ keyword: 'any' }), + statements: [ + tsc.returnStatement({ + expression: tsc.identifier({ text: refSymbol.placeholder }), + }), + ], + }), + ], + }); + ast.expression = lazyExpression; + ast.hasLazyExpression = true; + state.hasLazyExpression.value = true; } - - state.circularReferenceTracker.pop(); - state.currentReferenceTracker.pop(); } else if (schema.type) { - const zSchema = schemaTypeToZodSchema({ plugin, schema, state }); - zodSchema.expression = zSchema.expression; - zodSchema.hasCircularReference = zSchema.hasCircularReference; + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: schema as SchemaWithType, + state, + }); + ast.expression = typeAst.expression; + ast.hasLazyExpression = typeAst.hasLazyExpression; if (plugin.config.metadata && schema.description) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.register, }), parameters: [ tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.globalRegistry, }), tsc.objectExpression({ @@ -1137,11 +96,14 @@ const schemaToZodSchema = ({ schema = deduplicateSchema({ schema }); if (schema.items) { - const itemSchemas = schema.items.map((item) => - schemaToZodSchema({ + const itemSchemas = schema.items.map((item, index) => + irSchemaToAst({ plugin, schema: item, - state, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, }), ); @@ -1154,27 +116,27 @@ const schemaToZodSchema = ({ firstSchema.logicalOperator === 'or' || (firstSchema.type && firstSchema.type !== 'object') ) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.intersection, }), parameters: itemSchemas.map((schema) => schema.expression), }); } else { - zodSchema.expression = itemSchemas[0]!.expression; + ast.expression = itemSchemas[0]!.expression; itemSchemas.slice(1).forEach((schema) => { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.intersection, }), parameters: [ - zodSchema.expression, - schema.hasCircularReference + ast.expression, + schema.hasLazyExpression ? tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.lazy, }), parameters: [ @@ -1193,9 +155,9 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.union, }), parameters: [ @@ -1206,40 +168,40 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema = schemaToZodSchema({ plugin, schema, state }); + ast = irSchemaToAst({ plugin, schema, state }); } } else { // catch-all fallback for failed schemas - const zSchema = schemaTypeToZodSchema({ + const typeAst = irSchemaWithTypeToAst({ plugin, schema: { type: 'unknown', }, state, }); - zodSchema.expression = zSchema.expression; + ast.expression = typeAst.expression; } - if (zodSchema.expression) { + if (ast.expression) { if (schema.accessScope === 'read') { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.readonly, }), - parameters: [zodSchema.expression], + parameters: [ast.expression], }); } if (optional) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.optional, }), - parameters: [zodSchema.expression], + parameters: [ast.expression], }); - zodSchema.typeName = identifiers.ZodMiniOptional; + ast.typeName = identifiers.ZodMiniOptional; } if (schema.default !== undefined) { @@ -1249,71 +211,63 @@ const schemaToZodSchema = ({ value: schema.default, }); if (callParameter) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers._default, }), - parameters: [zodSchema.expression, callParameter], + parameters: [ast.expression, callParameter], }); } } } - return zodSchema as ZodSchema; + return ast as Ast; }; const handleComponent = ({ - id, + $ref, plugin, schema, - state: _state, -}: { - id: string; - plugin: ZodPlugin['Instance']; + state, +}: IrSchemaToAstOptions & { + $ref: string; schema: IR.SchemaObject; - state?: Omit; }): void => { - const state: State = { - circularReferenceTracker: [id], - hasCircularReference: false, - ..._state, - currentReferenceTracker: [id], - }; - - const selector = plugin.api.getSelector('ref', id); - let symbol = plugin.getSymbol(selector); - if (symbol && !plugin.getSymbolValue(symbol)) return; - - const zodSchema = schemaToZodSchema({ plugin, schema, state }); - const baseName = refToName(id); - symbol = plugin.registerSymbol({ + const ast = irSchemaToAst({ plugin, schema, state }); + const baseName = refToName($ref); + const resourceType = pathToSymbolResourceType(state._path.value); + const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.definitions, name: baseName, }), - selector, + selector: plugin.api.selector('ref', $ref), }); const typeInferSymbol = plugin.config.definitions.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.definitions.types.infer, name: baseName, }), - selector: plugin.api.getSelector('type-infer-ref', id), + selector: plugin.api.selector('type-infer-ref', $ref), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema, symbol, typeInferSymbol, - zodSchema, }); }; @@ -1322,7 +276,7 @@ export const handlerMini: ZodPlugin['Handler'] = ({ plugin }) => { external: getZodModule({ plugin }), meta: { importKind: 'namespace' }, name: 'z', - selector: plugin.api.getSelector('import', 'zod'), + selector: plugin.api.selector('external', 'zod.z'), }); plugin.forEach( @@ -1334,52 +288,68 @@ export const handlerMini: ZodPlugin['Handler'] = ({ plugin }) => { (event) => { switch (event.type) { case 'operation': - operationToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irOperationToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; case 'parameter': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.parameter.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'requestBody': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.requestBody.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'schema': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'webhook': - webhookToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irWebhookToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; } diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/array.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/array.ts new file mode 100644 index 0000000000..3b77176ce1 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/array.ts @@ -0,0 +1,173 @@ +import type ts from 'typescript'; + +import { deduplicateSchema } from '../../../../ir/schema'; +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; +import { unknownToAst } from './unknown'; + +export const arrayToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'array'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + const functionName = tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }); + + if (!schema.items) { + result.expression = tsc.callExpression({ + functionName, + parameters: [ + unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }).expression, + ], + }); + } else { + schema = deduplicateSchema({ schema }); + + // at least one item is guaranteed + const itemExpressions = schema.items!.map((item, index) => { + const itemAst = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + if (itemAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + return itemAst.expression; + }); + + if (itemExpressions.length === 1) { + result.expression = tsc.callExpression({ + functionName, + parameters: itemExpressions, + }); + } else { + 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 + // `.intersection()` as that does not exist on `.union()` and non-object schemas. + let intersectionExpression: ts.Expression; + if ( + firstSchema.logicalOperator === 'or' || + (firstSchema.type && firstSchema.type !== 'object') + ) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.intersection, + }), + parameters: itemExpressions, + }); + } else { + intersectionExpression = itemExpressions[0]!; + for (let i = 1; i < itemExpressions.length; i++) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.intersection, + }), + parameters: [intersectionExpression, itemExpressions[i]!], + }); + } + } + + result.expression = tsc.callExpression({ + functionName, + parameters: [intersectionExpression], + }); + } else { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemExpressions, + }), + ], + }), + ], + }); + } + } + } + + const checks: Array = []; + + if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }), + ); + } else { + if (schema.minItems !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.minLength, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }), + ); + } + + if (schema.maxItems !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.maxLength, + }), + parameters: [tsc.valueToExpression({ value: schema.maxItems })], + }), + ); + } + } + + if (checks.length) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.check, + }), + parameters: checks, + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts new file mode 100644 index 0000000000..657848a070 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts @@ -0,0 +1,34 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const booleanToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'boolean'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + if (typeof schema.const === 'boolean') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.boolean(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.boolean, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/enum.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/enum.ts new file mode 100644 index 0000000000..2b43769ba3 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/enum.ts @@ -0,0 +1,127 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { unknownToAst } from './unknown'; + +export const enumToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'enum'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + const enumMembers: Array = []; + const literalMembers: Array = []; + + let isNullable = false; + let allStrings = true; + + for (const item of schema.items ?? []) { + // Zod supports string, number, and boolean enums + if (item.type === 'string' && typeof item.const === 'string') { + const stringLiteral = tsc.stringLiteral({ + text: item.const, + }); + enumMembers.push(stringLiteral); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [stringLiteral], + }), + ); + } else if ( + (item.type === 'number' || item.type === 'integer') && + typeof item.const === 'number' + ) { + allStrings = false; + const numberLiteral = tsc.ots.number(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [numberLiteral], + }), + ); + } else if (item.type === 'boolean' && typeof item.const === 'boolean') { + allStrings = false; + const booleanLiteral = tsc.ots.boolean(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [booleanLiteral], + }), + ); + } else if (item.type === 'null' || item.const === null) { + isNullable = true; + } + } + + if (!literalMembers.length) { + return unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + } + + // Use z.enum() for pure string enums, z.union() for mixed or non-string types + if (allStrings && enumMembers.length > 0) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.enum, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: enumMembers, + multiLine: false, + }), + ], + }); + } else if (literalMembers.length === 1) { + // For single-member unions, use the member directly instead of wrapping in z.union() + result.expression = literalMembers[0]; + } else { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: literalMembers, + multiLine: literalMembers.length > 3, + }), + ], + }); + } + + if (isNullable) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.nullable, + }), + parameters: [result.expression], + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/index.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/index.ts new file mode 100644 index 0000000000..62c481bfd5 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/index.ts @@ -0,0 +1,85 @@ +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { arrayToAst } from './array'; +import { booleanToAst } from './boolean'; +import { enumToAst } from './enum'; +import { neverToAst } from './never'; +import { nullToAst } from './null'; +import { numberToAst } from './number'; +import { objectToAst } from './object'; +import { stringToAst } from './string'; +import { tupleToAst } from './tuple'; +import { undefinedToAst } from './undefined'; +import { unknownToAst } from './unknown'; +import { voidToAst } from './void'; + +export const irSchemaWithTypeToAst = ({ + schema, + ...args +}: IrSchemaToAstOptions & { + schema: SchemaWithType; +}): Omit => { + switch (schema.type) { + case 'array': + return arrayToAst({ + ...args, + schema: schema as SchemaWithType<'array'>, + }); + case 'boolean': + return booleanToAst({ + ...args, + schema: schema as SchemaWithType<'boolean'>, + }); + case 'enum': + return enumToAst({ + ...args, + schema: schema as SchemaWithType<'enum'>, + }); + case 'integer': + case 'number': + return numberToAst({ + ...args, + schema: schema as SchemaWithType<'integer' | 'number'>, + }); + case 'never': + return neverToAst({ + ...args, + schema: schema as SchemaWithType<'never'>, + }); + case 'null': + return nullToAst({ + ...args, + schema: schema as SchemaWithType<'null'>, + }); + case 'object': + return objectToAst({ + ...args, + schema: schema as SchemaWithType<'object'>, + }); + case 'string': + return stringToAst({ + ...args, + schema: schema as SchemaWithType<'string'>, + }); + case 'tuple': + return tupleToAst({ + ...args, + schema: schema as SchemaWithType<'tuple'>, + }); + case 'undefined': + return undefinedToAst({ + ...args, + schema: schema as SchemaWithType<'undefined'>, + }); + case 'unknown': + return unknownToAst({ + ...args, + schema: schema as SchemaWithType<'unknown'>, + }); + case 'void': + return voidToAst({ + ...args, + schema: schema as SchemaWithType<'void'>, + }); + } +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/never.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/never.ts new file mode 100644 index 0000000000..57e20c23a4 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/never.ts @@ -0,0 +1,20 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const neverToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'never'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const result: Partial> = {}; + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.never, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/null.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/null.ts new file mode 100644 index 0000000000..b3ff566dd0 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/null.ts @@ -0,0 +1,20 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const nullToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'null'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const result: Partial> = {}; + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.null, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/number.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/number.ts new file mode 100644 index 0000000000..e04617c615 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/number.ts @@ -0,0 +1,118 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import { numberParameter } from '../../shared/numbers'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const numberToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'integer' | 'number'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; + + if (typeof schema.const === 'number') { + // TODO: parser - handle bigint constants + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.number(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: isBigInt + ? tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.coerce, + }), + name: identifiers.bigint, + }) + : tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.number, + }), + }); + + if (!isBigInt && schema.type === 'integer') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.int, + }), + }); + } + + const checks: Array = []; + + if (schema.exclusiveMinimum !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.gt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMinimum }), + ], + }), + ); + } else if (schema.minimum !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.gte, + }), + parameters: [numberParameter({ isBigInt, value: schema.minimum })], + }), + ); + } + + if (schema.exclusiveMaximum !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.lt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMaximum }), + ], + }), + ); + } else if (schema.maximum !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.lte, + }), + parameters: [numberParameter({ isBigInt, value: schema.maximum })], + }), + ); + } + + if (checks.length) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.check, + }), + parameters: checks, + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/object.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/object.ts new file mode 100644 index 0000000000..9d066adb78 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/object.ts @@ -0,0 +1,129 @@ +import ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import { numberRegExp } from '../../../../utils/regexp'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const objectToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'object'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + // TODO: parser - handle constants + const properties: Array = + []; + + const required = schema.required ?? []; + + for (const name in schema.properties) { + const property = schema.properties[name]!; + const isRequired = required.includes(name); + + const propertyAst = irSchemaToAst({ + optional: !isRequired, + plugin, + schema: property, + state: { + ...state, + _path: toRef([...state._path.value, 'properties', name]), + }, + }); + if (propertyAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + + numberRegExp.lastIndex = 0; + let propertyName; + if (numberRegExp.test(name)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = name.startsWith('-') + ? ts.factory.createStringLiteral(name) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = 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}'`; + } + + if (propertyAst.hasLazyExpression) { + properties.push( + tsc.getAccessorDeclaration({ + name: propertyName, + statements: [ + tsc.returnStatement({ + expression: propertyAst.expression, + }), + ], + }), + ); + } else { + properties.push( + tsc.propertyAssignment({ + initializer: propertyAst.expression, + name: propertyName, + }), + ); + } + } + + if ( + schema.additionalProperties && + (!schema.properties || !Object.keys(schema.properties).length) + ) { + const additionalAst = irSchemaToAst({ + plugin, + schema: schema.additionalProperties, + state: { + ...state, + _path: toRef([...state._path.value, 'additionalProperties']), + }, + }); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.record, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.string, + }), + parameters: [], + }), + additionalAst.expression, + ], + }); + if (additionalAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.object, + }), + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], + }); + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts new file mode 100644 index 0000000000..4a4d98ce59 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts @@ -0,0 +1,192 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const stringToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'string'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + if (typeof schema.const === 'string') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.string(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.string, + }), + }); + + const dateTimeOptions: { key: string; value: boolean }[] = []; + + if (plugin.config.dates.offset) { + dateTimeOptions.push({ key: 'offset', value: true }); + } + if (plugin.config.dates.local) { + dateTimeOptions.push({ key: 'local', value: true }); + } + + if (schema.format) { + switch (schema.format) { + case 'date': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.date, + }), + }); + break; + case 'date-time': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.datetime, + }), + parameters: + dateTimeOptions.length > 0 + ? [ + tsc.objectExpression({ + obj: dateTimeOptions, + }), + ] + : [], + }); + break; + case 'email': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.email, + }), + }); + break; + case 'ipv4': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.ipv4, + }), + }); + break; + case 'ipv6': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.ipv6, + }), + }); + break; + case 'time': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.time, + }), + }); + break; + case 'uri': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.url, + }), + }); + break; + case 'uuid': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.uuid, + }), + }); + break; + } + } + + const checks: Array = []; + + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }), + ); + } else { + if (schema.minLength !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.minLength, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }), + ); + } + + if (schema.maxLength !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.maxLength, + }), + parameters: [tsc.valueToExpression({ value: schema.maxLength })], + }), + ); + } + } + + if (schema.pattern) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.regex, + }), + parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], + }), + ); + } + + if (checks.length) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.check, + }), + parameters: checks, + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/tuple.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/tuple.ts new file mode 100644 index 0000000000..0ea4ac4c89 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/tuple.ts @@ -0,0 +1,77 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const tupleToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'tuple'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + if (schema.const && Array.isArray(schema.const)) { + const tupleElements = schema.const.map((value) => + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.valueToExpression({ value })], + }), + ); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return result as Omit; + } + + const tupleElements: Array = []; + + if (schema.items) { + schema.items.forEach((item, index) => { + const itemSchema = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + tupleElements.push(itemSchema.expression); + if (itemSchema.hasLazyExpression) { + result.hasLazyExpression = true; + } + }); + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/undefined.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/undefined.ts new file mode 100644 index 0000000000..13cddcfa1b --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/undefined.ts @@ -0,0 +1,20 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const undefinedToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'undefined'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const result: Partial> = {}; + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.undefined, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/unknown.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/unknown.ts new file mode 100644 index 0000000000..2ada823cbf --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/unknown.ts @@ -0,0 +1,20 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const unknownToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'unknown'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const result: Partial> = {}; + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.unknown, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/void.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/void.ts new file mode 100644 index 0000000000..b15a428457 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/void.ts @@ -0,0 +1,20 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const voidToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'void'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const result: Partial> = {}; + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.void, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/plugin.ts b/packages/openapi-ts/src/plugins/zod/plugin.ts index fbd86a5a33..78e159140d 100644 --- a/packages/openapi-ts/src/plugins/zod/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/plugin.ts @@ -8,10 +8,9 @@ export const handler: ZodPlugin['Handler'] = (args) => { switch (plugin.config.compatibilityVersion) { case 3: return handlerV3(args); - case 4: - return handlerV4(args); case 'mini': return handlerMini(args); + case 4: default: return handlerV4(args); } diff --git a/packages/openapi-ts/src/plugins/zod/export.ts b/packages/openapi-ts/src/plugins/zod/shared/export.ts similarity index 64% rename from packages/openapi-ts/src/plugins/zod/export.ts rename to packages/openapi-ts/src/plugins/zod/shared/export.ts index 21c836849b..9fa2762eb5 100644 --- a/packages/openapi-ts/src/plugins/zod/export.ts +++ b/packages/openapi-ts/src/plugins/zod/shared/export.ts @@ -1,41 +1,39 @@ import type { Symbol } from '@hey-api/codegen-core'; import type ts from 'typescript'; -import type { IR } from '../../ir/types'; -import { tsc } from '../../tsc'; -import { createSchemaComment } from '../shared/utils/schema'; -import { identifiers } from './constants'; -import type { ZodSchema } from './shared/types'; -import type { ZodPlugin } from './types'; +import type { IR } from '../../../ir/types'; +import { tsc } from '../../../tsc'; +import { createSchemaComment } from '../../shared/utils/schema'; +import { identifiers } from '../constants'; +import type { ZodPlugin } from '../types'; +import type { Ast } from './types'; -export const exportZodSchema = ({ +export const exportAst = ({ + ast, plugin, schema, symbol, typeInferSymbol, - zodSchema, }: { + ast: Ast; plugin: ZodPlugin['Instance']; schema: IR.SchemaObject; symbol: Symbol; typeInferSymbol: Symbol | undefined; - zodSchema: ZodSchema; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); +}): void => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); const statement = tsc.constVariable({ comment: plugin.config.comments ? createSchemaComment({ schema }) : undefined, exportConst: symbol.exported, - expression: zodSchema.expression, + expression: ast.expression, name: symbol.placeholder, - typeName: zodSchema.typeName + typeName: ast.typeName ? (tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: zodSchema.typeName, + expression: z.placeholder, + name: ast.typeName, }) as unknown as ts.TypeNode) : undefined, }); @@ -52,7 +50,7 @@ export const exportZodSchema = ({ }) as unknown as ts.TypeNode, ], typeName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.infer, }) as unknown as string, }), diff --git a/packages/openapi-ts/src/plugins/zod/shared/numbers.ts b/packages/openapi-ts/src/plugins/zod/shared/numbers.ts new file mode 100644 index 0000000000..63e44916d1 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/shared/numbers.ts @@ -0,0 +1,28 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../tsc'; + +export const numberParameter = ({ + isBigInt, + value, +}: { + isBigInt: boolean; + value: unknown; +}): ts.Expression | undefined => { + const expression = tsc.valueToExpression({ value }); + + if ( + isBigInt && + (typeof value === 'bigint' || + typeof value === 'number' || + typeof value === 'string' || + typeof value === 'boolean') + ) { + return tsc.callExpression({ + functionName: 'BigInt', + parameters: [expression], + }); + } + + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/shared/operation.ts b/packages/openapi-ts/src/plugins/zod/shared/operation.ts index b4b4f7dd14..2d5187f42f 100644 --- a/packages/openapi-ts/src/plugins/zod/shared/operation.ts +++ b/packages/openapi-ts/src/plugins/zod/shared/operation.ts @@ -1,19 +1,23 @@ import { operationResponsesMap } from '../../../ir/operation'; import type { IR } from '../../../ir/types'; import { buildName } from '../../../openApi/shared/utils/name'; -import { exportZodSchema } from '../export'; -import type { ZodPlugin } from '../types'; -import type { ZodSchema } from './types'; +import { pathToSymbolResourceType } from '../../shared/utils/meta'; +import { exportAst } from './export'; +import type { Ast, IrSchemaToAstOptions } from './types'; -export const operationToZodSchema = ({ - getZodSchema, +export const irOperationToAst = ({ + getAst, operation, plugin, -}: { - getZodSchema: (schema: IR.SchemaObject) => ZodSchema; + state, +}: Omit & { + getAst: ( + schema: IR.SchemaObject, + path: ReadonlyArray, + ) => Ast; operation: IR.OperationObject; - plugin: ZodPlugin['Instance']; -}) => { + state: Partial; +}): void => { if (plugin.config.requests.enabled) { const requiredProperties = new Set(); @@ -112,34 +116,40 @@ export const operationToZodSchema = ({ schemaData.required = [...requiredProperties]; - const zodSchema = getZodSchema(schemaData); + const path = state._path?.value || []; + const ast = getAst(schemaData, path); + const resourceType = pathToSymbolResourceType(path); const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.requests, name: operation.id, }), - selector: plugin.api.getSelector('data', operation.id), + selector: plugin.api.selector('data', operation.id), }); const typeInferSymbol = plugin.config.requests.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.requests.types.infer, name: operation.id, }), - selector: plugin.api.getSelector('type-infer-data', operation.id), + selector: plugin.api.selector('type-infer-data', operation.id), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema: schemaData, symbol, typeInferSymbol, - zodSchema, }); } @@ -148,37 +158,43 @@ export const operationToZodSchema = ({ const { response } = operationResponsesMap(operation); if (response) { - const zodSchema = getZodSchema(response); + const path = [...(state._path?.value || []), 'responses']; + const ast = getAst(response, path); + const resourceType = pathToSymbolResourceType(path); const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.responses, name: operation.id, }), - selector: plugin.api.getSelector('responses', operation.id), + selector: plugin.api.selector('responses', operation.id), }); const typeInferSymbol = plugin.config.responses.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.responses.types.infer, name: operation.id, }), - selector: plugin.api.getSelector( + selector: plugin.api.selector( 'type-infer-responses', operation.id, ), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema: response, symbol, typeInferSymbol, - zodSchema, }); } } diff --git a/packages/openapi-ts/src/plugins/zod/shared/types.d.ts b/packages/openapi-ts/src/plugins/zod/shared/types.d.ts index 6f352346fc..197cea2d5f 100644 --- a/packages/openapi-ts/src/plugins/zod/shared/types.d.ts +++ b/packages/openapi-ts/src/plugins/zod/shared/types.d.ts @@ -1,25 +1,29 @@ import type ts from 'typescript'; import type { IR } from '../../../ir/types'; +import type { ToRefs } from '../../shared/types/refs'; +import type { ZodPlugin } from '../types'; -export interface SchemaWithType['type']> - extends Omit { - type: Extract['type'], T>; -} +export type Ast = { + expression: ts.Expression; + hasLazyExpression?: boolean; + typeName?: string | ts.Identifier; +}; -export type State = { - circularReferenceTracker: Array; +export type IrSchemaToAstOptions = { + plugin: ZodPlugin['Instance']; + state: ToRefs; +}; + +export type PluginState = { /** - * Works the same as `circularReferenceTracker`, but it resets whenever we - * walk inside another schema. This can be used to detect if a schema - * directly references itself. + * Path to the schema in the intermediary representation. */ - currentReferenceTracker: Array; - hasCircularReference: boolean; + _path: ReadonlyArray; + hasLazyExpression: boolean; }; -export type ZodSchema = { - expression: ts.Expression; - hasCircularReference?: boolean; - typeName?: string | ts.Identifier; +export type ValidatorArgs = { + operation: IR.OperationObject; + plugin: ZodPlugin['Instance']; }; diff --git a/packages/openapi-ts/src/plugins/zod/shared/webhook.ts b/packages/openapi-ts/src/plugins/zod/shared/webhook.ts index fc753cffb8..74926f8944 100644 --- a/packages/openapi-ts/src/plugins/zod/shared/webhook.ts +++ b/packages/openapi-ts/src/plugins/zod/shared/webhook.ts @@ -1,17 +1,21 @@ import type { IR } from '../../../ir/types'; import { buildName } from '../../../openApi/shared/utils/name'; -import { exportZodSchema } from '../export'; -import type { ZodPlugin } from '../types'; -import type { ZodSchema } from './types'; +import { pathToSymbolResourceType } from '../../shared/utils/meta'; +import { exportAst } from './export'; +import type { Ast, IrSchemaToAstOptions } from './types'; -export const webhookToZodSchema = ({ - getZodSchema, +export const irWebhookToAst = ({ + getAst, operation, plugin, -}: { - getZodSchema: (schema: IR.SchemaObject) => ZodSchema; + state, +}: Omit & { + getAst: ( + schema: IR.SchemaObject, + path: ReadonlyArray, + ) => Ast; operation: IR.OperationObject; - plugin: ZodPlugin['Instance']; + state: Partial; }) => { if (plugin.config.webhooks.enabled) { const requiredProperties = new Set(); @@ -111,37 +115,43 @@ export const webhookToZodSchema = ({ schemaData.required = [...requiredProperties]; - const zodSchema = getZodSchema(schemaData); + const path = state._path?.value || []; + const ast = getAst(schemaData, path); + const resourceType = pathToSymbolResourceType(path); const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.webhooks, name: operation.id, }), - selector: plugin.api.getSelector('webhook-request', operation.id), + selector: plugin.api.selector('webhook-request', operation.id), }); const typeInferSymbol = plugin.config.webhooks.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.webhooks.types.infer, name: operation.id, }), - selector: plugin.api.getSelector( + selector: plugin.api.selector( 'type-infer-webhook-request', operation.id, ), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema: schemaData, symbol, typeInferSymbol, - zodSchema, }); } }; diff --git a/packages/openapi-ts/src/plugins/zod/types.d.ts b/packages/openapi-ts/src/plugins/zod/types.d.ts index e5bc03a25c..ab95797aa9 100644 --- a/packages/openapi-ts/src/plugins/zod/types.d.ts +++ b/packages/openapi-ts/src/plugins/zod/types.d.ts @@ -96,7 +96,7 @@ export type UserConfig = Plugin.Name<'zod'> & */ types?: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. * * Can be: * - `boolean`: Shorthand for `{ enabled: boolean }` @@ -189,7 +189,7 @@ export type UserConfig = Plugin.Name<'zod'> & */ types?: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. * * Can be: * - `boolean`: Shorthand for `{ enabled: boolean }` @@ -267,7 +267,7 @@ export type UserConfig = Plugin.Name<'zod'> & */ types?: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. * * Can be: * - `boolean`: Shorthand for `{ enabled: boolean }` @@ -309,7 +309,7 @@ export type UserConfig = Plugin.Name<'zod'> & */ types?: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. * * Can be: * - `boolean`: Shorthand for `{ enabled: boolean }` @@ -378,7 +378,7 @@ export type UserConfig = Plugin.Name<'zod'> & */ types?: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. * * Can be: * - `boolean`: Shorthand for `{ enabled: boolean }` @@ -499,7 +499,7 @@ export type Config = Plugin.Name<'zod'> & */ types: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. */ infer: { /** @@ -572,7 +572,7 @@ export type Config = Plugin.Name<'zod'> & */ types: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. */ infer: { /** @@ -630,7 +630,7 @@ export type Config = Plugin.Name<'zod'> & */ types: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. */ infer: { /** @@ -662,7 +662,7 @@ export type Config = Plugin.Name<'zod'> & */ types: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. */ infer: { /** @@ -711,7 +711,7 @@ export type Config = Plugin.Name<'zod'> & */ types: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. */ infer: { /** diff --git a/packages/openapi-ts/src/plugins/zod/v3/api.ts b/packages/openapi-ts/src/plugins/zod/v3/api.ts new file mode 100644 index 0000000000..3328950cda --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/api.ts @@ -0,0 +1,71 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../tsc'; +import { identifiers } from '../constants'; +import type { ValidatorArgs } from '../shared/types'; + +export const createRequestValidatorV3 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol(plugin.api.selector('data', operation.id)); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; + +export const createResponseValidatorV3 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol( + plugin.api.selector('responses', operation.id), + ); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/plugin.ts b/packages/openapi-ts/src/plugins/zod/v3/plugin.ts index ab7fc55358..bcdbb22ebb 100644 --- a/packages/openapi-ts/src/plugins/zod/v3/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/v3/plugin.ts @@ -1,1039 +1,78 @@ -import ts from 'typescript'; - import { deduplicateSchema } from '../../../ir/schema'; import type { IR } from '../../../ir/types'; import { buildName } from '../../../openApi/shared/utils/name'; import { tsc } from '../../../tsc'; import { refToName } from '../../../utils/ref'; -import { numberRegExp } from '../../../utils/regexp'; +import type { SchemaWithType } from '../../shared/types/schema'; +import { pathToSymbolResourceType } from '../../shared/utils/meta'; +import { toRef, toRefs } from '../../shared/utils/refs'; import { identifiers } from '../constants'; -import { exportZodSchema } from '../export'; +import { exportAst } from '../shared/export'; import { getZodModule } from '../shared/module'; -import { operationToZodSchema } from '../shared/operation'; -import type { SchemaWithType, State, ZodSchema } from '../shared/types'; -import { webhookToZodSchema } from '../shared/webhook'; +import { numberParameter } from '../shared/numbers'; +import { irOperationToAst } from '../shared/operation'; +import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; +import { irWebhookToAst } from '../shared/webhook'; import type { ZodPlugin } from '../types'; +import { irSchemaWithTypeToAst } from './toAst'; -const arrayTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'array'>; - state: State; -}): Omit & { - anyType?: string; -} => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const functionName = tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }); - - let arrayExpression: ts.CallExpression | undefined; - let hasCircularReference = false; - - if (!schema.items) { - arrayExpression = tsc.callExpression({ - functionName, - parameters: [ - unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }), - ], - }); - } else { - schema = deduplicateSchema({ schema }); - - // at least one item is guaranteed - const itemExpressions = schema.items!.map((item) => { - const zodSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - if (zodSchema.hasCircularReference) { - hasCircularReference = true; - } - return zodSchema.expression; - }); - - if (itemExpressions.length === 1) { - arrayExpression = tsc.callExpression({ - functionName, - parameters: itemExpressions, - }); - } else { - 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 - // `.and()` as that does not exist on `.union()` and non-object schemas. - let intersectionExpression: ts.Expression; - if ( - firstSchema.logicalOperator === 'or' || - (firstSchema.type && firstSchema.type !== 'object') - ) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.intersection, - }), - parameters: itemExpressions, - }); - } else { - intersectionExpression = itemExpressions[0]!; - for (let i = 1; i < itemExpressions.length; i++) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: intersectionExpression, - name: identifiers.and, - }), - parameters: [itemExpressions[i]!], - }); - } - } - - arrayExpression = tsc.callExpression({ - functionName, - parameters: [intersectionExpression], - }); - } else { - arrayExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemExpressions, - }), - ], - }), - ], - }); - } - } - } - - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { - arrayExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: arrayExpression, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - } else { - if (schema.minItems !== undefined) { - arrayExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: arrayExpression, - name: identifiers.min, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - } - - if (schema.maxItems !== undefined) { - arrayExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: arrayExpression, - name: identifiers.max, - }), - parameters: [tsc.valueToExpression({ value: schema.maxItems })], - }); - } - } - - return { - expression: arrayExpression, - hasCircularReference, - }; -}; - -const booleanTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'boolean'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (typeof schema.const === 'boolean') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); - return expression; - } - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.boolean, - }), - }); - return expression; -}; - -const enumTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'enum'>; -}): ts.CallExpression => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const enumMembers: Array = []; - const literalMembers: Array = []; - - let isNullable = false; - let allStrings = true; - - for (const item of schema.items ?? []) { - // Zod supports string, number, and boolean enums - if (item.type === 'string' && typeof item.const === 'string') { - const stringLiteral = tsc.stringLiteral({ - text: item.const, - }); - enumMembers.push(stringLiteral); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [stringLiteral], - }), - ); - } else if ( - (item.type === 'number' || item.type === 'integer') && - typeof item.const === 'number' - ) { - allStrings = false; - const numberLiteral = tsc.ots.number(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [numberLiteral], - }), - ); - } else if (item.type === 'boolean' && typeof item.const === 'boolean') { - allStrings = false; - const booleanLiteral = tsc.ots.boolean(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [booleanLiteral], - }), - ); - } else if (item.type === 'null' || item.const === null) { - isNullable = true; - } - } - - if (!literalMembers.length) { - return unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }); - } - - // Use z.enum() for pure string enums, z.union() for mixed or non-string types - let enumExpression: ts.CallExpression; - if (allStrings && enumMembers.length > 0) { - enumExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.enum, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: enumMembers, - multiLine: false, - }), - ], - }); - } else if (literalMembers.length === 1) { - // For single-member unions, use the member directly instead of wrapping in z.union() - enumExpression = literalMembers[0]!; - } else { - enumExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: literalMembers, - multiLine: literalMembers.length > 3, - }), - ], - }); - } - - if (isNullable) { - enumExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: enumExpression, - name: identifiers.nullable, - }), - }); - } - - return enumExpression; -}; - -const neverTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'never'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.never, - }), - }); - return expression; -}; - -const nullTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'null'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.null, - }), - }); - return expression; -}; - -const numberParameter = ({ - isBigInt, - value, -}: { - isBigInt: boolean; - value: unknown; -}) => { - const expression = tsc.valueToExpression({ value }); - - if ( - isBigInt && - (typeof value === 'bigint' || - typeof value === 'number' || - typeof value === 'string' || - typeof value === 'boolean') - ) { - return tsc.callExpression({ - functionName: 'BigInt', - parameters: [expression], - }); - } - - return expression; -}; - -const numberTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'integer' | 'number'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const isBigInt = schema.type === 'integer' && schema.format === 'int64'; - - if (typeof schema.const === 'number') { - // TODO: parser - handle bigint constants - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.number(schema.const)], - }); - return expression; - } - - let numberExpression = tsc.callExpression({ - functionName: isBigInt - ? tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.coerce, - }), - name: identifiers.bigint, - }) - : tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.number, - }), - }); - - if (!isBigInt && schema.type === 'integer') { - numberExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: numberExpression, - name: identifiers.int, - }), - }); - } - - if (schema.exclusiveMinimum !== undefined) { - numberExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: numberExpression, - name: identifiers.gt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMinimum }), - ], - }); - } else if (schema.minimum !== undefined) { - numberExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: numberExpression, - name: identifiers.gte, - }), - parameters: [numberParameter({ isBigInt, value: schema.minimum })], - }); - } - - if (schema.exclusiveMaximum !== undefined) { - numberExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: numberExpression, - name: identifiers.lt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMaximum }), - ], - }); - } else if (schema.maximum !== undefined) { - numberExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: numberExpression, - name: identifiers.lte, - }), - parameters: [numberParameter({ isBigInt, value: schema.maximum })], - }); - } - - return numberExpression; -}; - -const objectTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'object'>; - state: State; -}): Omit & { - anyType?: string; -} => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - let hasCircularReference = false; - - // TODO: parser - handle constants - const properties: Array = []; - - const required = schema.required ?? []; - - for (const name in schema.properties) { - const property = schema.properties[name]!; - const isRequired = required.includes(name); - - const propertyExpression = schemaToZodSchema({ - optional: !isRequired, - plugin, - schema: property, - state, - }); - - if (propertyExpression.hasCircularReference) { - hasCircularReference = true; - } - - numberRegExp.lastIndex = 0; - let propertyName; - if (numberRegExp.test(name)) { - // For numeric literals, we'll handle negative numbers by using a string literal - // instead of trying to use a PrefixUnaryExpression - propertyName = name.startsWith('-') - ? ts.factory.createStringLiteral(name) - : ts.factory.createNumericLiteral(name); - } else { - propertyName = 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( - tsc.propertyAssignment({ - initializer: propertyExpression.expression, - name: propertyName, - }), - ); - } - - if ( - schema.additionalProperties && - (!schema.properties || !Object.keys(schema.properties).length) - ) { - const zodSchema = schemaToZodSchema({ - plugin, - schema: schema.additionalProperties, - state, - }); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.record, - }), - parameters: [zodSchema.expression], - }); - return { - anyType: 'AnyZodObject', - expression, - hasCircularReference: zodSchema.hasCircularReference, - }; - } - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.object, - }), - parameters: [ts.factory.createObjectLiteralExpression(properties, true)], - }); - return { - anyType: 'AnyZodObject', - expression, - hasCircularReference, - }; -}; - -const stringTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'string'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (typeof schema.const === 'string') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); - return expression; - } - - let stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.string, - }), - }); - - const dateTimeOptions: { key: string; value: boolean }[] = []; - - if (plugin.config.dates.offset) { - dateTimeOptions.push({ key: 'offset', value: true }); - } - if (plugin.config.dates.local) { - dateTimeOptions.push({ key: 'local', value: true }); - } - - if (schema.format) { - switch (schema.format) { - case 'date': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.date, - }), - }); - break; - case 'date-time': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.datetime, - }), - parameters: - dateTimeOptions.length > 0 - ? [ - tsc.objectExpression({ - obj: dateTimeOptions, - }), - ] - : [], - }); - break; - case 'email': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.email, - }), - }); - break; - case 'ipv4': - case 'ipv6': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.ip, - }), - }); - break; - case 'time': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.time, - }), - }); - break; - case 'uri': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.url, - }), - }); - break; - case 'uuid': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.uuid, - }), - }); - break; - } - } - - if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - } else { - if (schema.minLength !== undefined) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.min, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - } - - if (schema.maxLength !== undefined) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.max, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }); - } - } - - if (schema.pattern) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }); - } - - return stringExpression; -}; - -const tupleTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'tuple'>; - state: State; -}): Omit & { - anyType?: string; -} => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - let hasCircularReference = false; - - if (schema.const && Array.isArray(schema.const)) { - const tupleElements = schema.const.map((value) => - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.valueToExpression({ value })], - }), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return { - expression, - hasCircularReference, - }; - } - - const tupleElements: Array = []; - - for (const item of schema.items ?? []) { - const zodSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - if (zodSchema.hasCircularReference) { - hasCircularReference = true; - } - tupleElements.push(zodSchema.expression); - } - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return { - expression, - hasCircularReference, - }; -}; - -const undefinedTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'undefined'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.undefined, - }), - }); - return expression; -}; - -const unknownTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'unknown'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.unknown, - }), - }); - return expression; -}; - -const voidTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'void'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.void, - }), - }); - return expression; -}; - -const schemaTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: IR.SchemaObject; - state: State; -}): Omit & { - anyType?: string; -} => { - switch (schema.type as Required['type']) { - case 'array': - return arrayTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'array'>, - state, - }); - case 'boolean': - return { - expression: booleanTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'boolean'>, - }), - }; - case 'enum': - return { - expression: enumTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'enum'>, - }), - }; - case 'integer': - case 'number': - return { - expression: numberTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'integer' | 'number'>, - }), - }; - case 'never': - return { - expression: neverTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'never'>, - }), - }; - case 'null': - return { - expression: nullTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'null'>, - }), - }; - case 'object': - return objectTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'object'>, - state, - }); - case 'string': - return { - expression: stringTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'string'>, - }), - }; - case 'tuple': - return tupleTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'tuple'>, - state, - }); - case 'undefined': - return { - expression: undefinedTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'undefined'>, - }), - }; - case 'unknown': - return { - expression: unknownTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'unknown'>, - }), - }; - case 'void': - return { - expression: voidTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'void'>, - }), - }; - } -}; - -const schemaToZodSchema = ({ +export const irSchemaToAst = ({ optional, plugin, schema, state, -}: { +}: IrSchemaToAstOptions & { /** * Accept `optional` to handle optional object properties. We can't handle * this inside the object function because `.optional()` must come before * `.default()` which is handled in this function. */ optional?: boolean; - plugin: ZodPlugin['Instance']; schema: IR.SchemaObject; - state: State; -}): ZodSchema => { - let zodSchema: Partial = {}; +}): Ast => { + let ast: Partial = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); if (schema.$ref) { - const isCircularReference = state.circularReferenceTracker.includes( - schema.$ref, - ); - state.circularReferenceTracker.push(schema.$ref); - state.currentReferenceTracker.push(schema.$ref); - - const selector = plugin.api.getSelector('ref', schema.$ref); - let symbol = plugin.getSymbol(selector); - - if (isCircularReference) { - if (!symbol) { - symbol = plugin.referenceSymbol(selector); - } - - zodSchema.expression = tsc.callExpression({ + const selector = plugin.api.selector('ref', schema.$ref); + const refSymbol = plugin.referenceSymbol(selector); + if (plugin.isSymbolRegistered(selector)) { + const ref = tsc.identifier({ text: refSymbol.placeholder }); + ast.expression = ref; + } else { + const lazyExpression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.lazy, }), parameters: [ tsc.arrowFunction({ statements: [ tsc.returnStatement({ - expression: tsc.identifier({ text: symbol.placeholder }), + expression: tsc.identifier({ text: refSymbol.placeholder }), }), ], }), ], }); - zodSchema.hasCircularReference = schema.circular; - } else { - if (!symbol) { - // if $ref hasn't been processed yet, inline it to avoid the - // "Block-scoped variable used before its declaration." error - // this could be (maybe?) fixed by reshuffling the generation order - const ref = plugin.context.resolveIrRef(schema.$ref); - handleComponent({ - id: schema.$ref, - plugin, - schema: ref, - state: { - ...state, - currentReferenceTracker: [schema.$ref], - }, - }); - } else { - zodSchema.hasCircularReference = schema.circular; - } - - const refSymbol = plugin.referenceSymbol(selector); - zodSchema.expression = tsc.identifier({ text: refSymbol.placeholder }); + ast.expression = lazyExpression; + ast.hasLazyExpression = true; + state.hasLazyExpression.value = true; } - - state.circularReferenceTracker.pop(); - state.currentReferenceTracker.pop(); } else if (schema.type) { - const zSchema = schemaTypeToZodSchema({ plugin, schema, state }); - zodSchema.expression = zSchema.expression; - zodSchema.hasCircularReference = zSchema.hasCircularReference; - zodSchema.typeName = zSchema.anyType; + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: schema as SchemaWithType, + state, + }); + ast.expression = typeAst.expression; + ast.typeName = typeAst.anyType; if (plugin.config.metadata && schema.description) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.describe, }), parameters: [tsc.stringLiteral({ text: schema.description })], @@ -1043,16 +82,16 @@ const schemaToZodSchema = ({ schema = deduplicateSchema({ schema }); if (schema.items) { - const itemTypes = schema.items.map((item) => { - const zSchema = schemaToZodSchema({ + const itemTypes = schema.items.map((item, index) => { + const typeAst = irSchemaToAst({ plugin, schema: item, - state, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, }); - if (zSchema.hasCircularReference) { - zodSchema.hasCircularReference = true; - } - return zSchema.expression; + return typeAst.expression; }); if (schema.logicalOperator === 'and') { @@ -1064,19 +103,19 @@ const schemaToZodSchema = ({ firstSchema.logicalOperator === 'or' || (firstSchema.type && firstSchema.type !== 'object') ) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.intersection, }), parameters: itemTypes, }); } else { - zodSchema.expression = itemTypes[0]; + ast.expression = itemTypes[0]; itemTypes.slice(1).forEach((item) => { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression!, + expression: ast.expression!, name: identifiers.and, }), parameters: [item], @@ -1084,9 +123,9 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.union, }), parameters: [ @@ -1097,36 +136,35 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema = schemaToZodSchema({ plugin, schema, state }); + ast = irSchemaToAst({ plugin, schema, state }); } } else { // catch-all fallback for failed schemas - const zSchema = schemaTypeToZodSchema({ + const typeAst = irSchemaWithTypeToAst({ plugin, schema: { type: 'unknown', }, state, }); - zodSchema.expression = zSchema.expression; - zodSchema.hasCircularReference = zSchema.hasCircularReference; - zodSchema.typeName = zSchema.anyType; + ast.expression = typeAst.expression; + ast.typeName = typeAst.anyType; } - if (zodSchema.expression) { + if (ast.expression) { if (schema.accessScope === 'read') { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.readonly, }), }); } if (optional) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.optional, }), }); @@ -1139,9 +177,9 @@ const schemaToZodSchema = ({ value: schema.default, }); if (callParameter) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.default, }), parameters: [callParameter], @@ -1150,69 +188,60 @@ const schemaToZodSchema = ({ } } - if (zodSchema.hasCircularReference) { - if (!zodSchema.typeName) { - zodSchema.typeName = 'ZodTypeAny'; + if (state.hasLazyExpression.value) { + if (!ast.typeName) { + ast.typeName = 'ZodTypeAny'; } - } else if (zodSchema.typeName) { - zodSchema.typeName = undefined; + } else if (ast.typeName) { + ast.typeName = undefined; } - return zodSchema as ZodSchema; + return ast as Ast; }; const handleComponent = ({ - id, + $ref, plugin, schema, state, -}: { - id: string; - plugin: ZodPlugin['Instance']; +}: IrSchemaToAstOptions & { + $ref: string; schema: IR.SchemaObject; - state?: State; }): void => { - if (!state) { - state = { - circularReferenceTracker: [id], - currentReferenceTracker: [id], - hasCircularReference: false, - }; - } - - const selector = plugin.api.getSelector('ref', id); - let symbol = plugin.getSymbol(selector); - if (symbol) return; - - const zodSchema = schemaToZodSchema({ plugin, schema, state }); - const baseName = refToName(id); - symbol = plugin.registerSymbol({ + const ast = irSchemaToAst({ plugin, schema, state }); + const baseName = refToName($ref); + const resourceType = pathToSymbolResourceType(state._path.value); + const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.definitions, name: baseName, }), - selector, + selector: plugin.api.selector('ref', $ref), }); const typeInferSymbol = plugin.config.definitions.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.definitions.types.infer, name: baseName, }), - selector: plugin.api.getSelector('type-infer-ref', id), + selector: plugin.api.selector('type-infer-ref', $ref), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema, symbol, typeInferSymbol, - zodSchema, }); }; @@ -1220,7 +249,7 @@ export const handlerV3: ZodPlugin['Handler'] = ({ plugin }) => { plugin.registerSymbol({ external: getZodModule({ plugin }), name: 'z', - selector: plugin.api.getSelector('import', 'zod'), + selector: plugin.api.selector('external', 'zod.z'), }); plugin.forEach( @@ -1232,52 +261,68 @@ export const handlerV3: ZodPlugin['Handler'] = ({ plugin }) => { (event) => { switch (event.type) { case 'operation': - operationToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irOperationToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; case 'parameter': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.parameter.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'requestBody': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.requestBody.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'schema': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'webhook': - webhookToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irWebhookToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; } diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/array.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/array.ts new file mode 100644 index 0000000000..9f3c1f897a --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/array.ts @@ -0,0 +1,161 @@ +import type ts from 'typescript'; + +import { deduplicateSchema } from '../../../../ir/schema'; +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; +import { unknownToAst } from './unknown'; + +export const arrayToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'array'>; +}): Omit & { + anyType?: string; +} => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const functionName = tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }); + + let arrayExpression: ts.CallExpression | undefined; + let hasLazyExpression = false; + + if (!schema.items) { + arrayExpression = tsc.callExpression({ + functionName, + parameters: [ + unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }), + ], + }); + } else { + schema = deduplicateSchema({ schema }); + + // at least one item is guaranteed + const itemExpressions = schema.items!.map((item, index) => { + const itemAst = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + if (itemAst.hasLazyExpression) { + hasLazyExpression = true; + } + return itemAst.expression; + }); + + if (itemExpressions.length === 1) { + arrayExpression = tsc.callExpression({ + functionName, + parameters: itemExpressions, + }); + } else { + 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 + // `.and()` as that does not exist on `.union()` and non-object schemas. + let intersectionExpression: ts.Expression; + if ( + firstSchema.logicalOperator === 'or' || + (firstSchema.type && firstSchema.type !== 'object') + ) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.intersection, + }), + parameters: itemExpressions, + }); + } else { + intersectionExpression = itemExpressions[0]!; + for (let i = 1; i < itemExpressions.length; i++) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: intersectionExpression, + name: identifiers.and, + }), + parameters: [itemExpressions[i]!], + }); + } + } + + arrayExpression = tsc.callExpression({ + functionName, + parameters: [intersectionExpression], + }); + } else { + arrayExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemExpressions, + }), + ], + }), + ], + }); + } + } + } + + if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { + arrayExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: arrayExpression, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + } else { + if (schema.minItems !== undefined) { + arrayExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: arrayExpression, + name: identifiers.min, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + } + + if (schema.maxItems !== undefined) { + arrayExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: arrayExpression, + name: identifiers.max, + }), + parameters: [tsc.valueToExpression({ value: schema.maxItems })], + }); + } + } + + return { + expression: arrayExpression, + hasLazyExpression, + }; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts new file mode 100644 index 0000000000..7562823d57 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts @@ -0,0 +1,32 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const booleanToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'boolean'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (typeof schema.const === 'boolean') { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.boolean(schema.const)], + }); + return expression; + } + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.boolean, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/enum.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/enum.ts new file mode 100644 index 0000000000..d735f09fb4 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/enum.ts @@ -0,0 +1,125 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { unknownToAst } from './unknown'; + +export const enumToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'enum'>; +}): ts.CallExpression => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const enumMembers: Array = []; + const literalMembers: Array = []; + + let isNullable = false; + let allStrings = true; + + for (const item of schema.items ?? []) { + // Zod supports string, number, and boolean enums + if (item.type === 'string' && typeof item.const === 'string') { + const stringLiteral = tsc.stringLiteral({ + text: item.const, + }); + enumMembers.push(stringLiteral); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [stringLiteral], + }), + ); + } else if ( + (item.type === 'number' || item.type === 'integer') && + typeof item.const === 'number' + ) { + allStrings = false; + const numberLiteral = tsc.ots.number(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [numberLiteral], + }), + ); + } else if (item.type === 'boolean' && typeof item.const === 'boolean') { + allStrings = false; + const booleanLiteral = tsc.ots.boolean(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [booleanLiteral], + }), + ); + } else if (item.type === 'null' || item.const === null) { + isNullable = true; + } + } + + if (!literalMembers.length) { + return unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + } + + // Use z.enum() for pure string enums, z.union() for mixed or non-string types + let enumExpression: ts.CallExpression; + if (allStrings && enumMembers.length > 0) { + enumExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.enum, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: enumMembers, + multiLine: false, + }), + ], + }); + } else if (literalMembers.length === 1) { + // For single-member unions, use the member directly instead of wrapping in z.union() + enumExpression = literalMembers[0]!; + } else { + enumExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: literalMembers, + multiLine: literalMembers.length > 3, + }), + ], + }); + } + + if (isNullable) { + enumExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: enumExpression, + name: identifiers.nullable, + }), + }); + } + + return enumExpression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/index.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/index.ts new file mode 100644 index 0000000000..50790d8c71 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/index.ts @@ -0,0 +1,105 @@ +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { arrayToAst } from './array'; +import { booleanToAst } from './boolean'; +import { enumToAst } from './enum'; +import { neverToAst } from './never'; +import { nullToAst } from './null'; +import { numberToAst } from './number'; +import { objectToAst } from './object'; +import { stringToAst } from './string'; +import { tupleToAst } from './tuple'; +import { undefinedToAst } from './undefined'; +import { unknownToAst } from './unknown'; +import { voidToAst } from './void'; + +export const irSchemaWithTypeToAst = ({ + schema, + ...args +}: IrSchemaToAstOptions & { + schema: SchemaWithType; +}): Omit & { + anyType?: string; +} => { + switch (schema.type) { + case 'array': + return arrayToAst({ + ...args, + schema: schema as SchemaWithType<'array'>, + }); + case 'boolean': + return { + expression: booleanToAst({ + ...args, + schema: schema as SchemaWithType<'boolean'>, + }), + }; + case 'enum': + return { + expression: enumToAst({ + ...args, + schema: schema as SchemaWithType<'enum'>, + }), + }; + case 'integer': + case 'number': + return { + expression: numberToAst({ + ...args, + schema: schema as SchemaWithType<'integer' | 'number'>, + }), + }; + case 'never': + return { + expression: neverToAst({ + ...args, + schema: schema as SchemaWithType<'never'>, + }), + }; + case 'null': + return { + expression: nullToAst({ + ...args, + schema: schema as SchemaWithType<'null'>, + }), + }; + case 'object': + return objectToAst({ + ...args, + schema: schema as SchemaWithType<'object'>, + }); + case 'string': + return { + expression: stringToAst({ + ...args, + schema: schema as SchemaWithType<'string'>, + }), + }; + case 'tuple': + return tupleToAst({ + ...args, + schema: schema as SchemaWithType<'tuple'>, + }); + case 'undefined': + return { + expression: undefinedToAst({ + ...args, + schema: schema as SchemaWithType<'undefined'>, + }), + }; + case 'unknown': + return { + expression: unknownToAst({ + ...args, + schema: schema as SchemaWithType<'unknown'>, + }), + }; + case 'void': + return { + expression: voidToAst({ + ...args, + schema: schema as SchemaWithType<'void'>, + }), + }; + } +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/never.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/never.ts new file mode 100644 index 0000000000..3dbddf9740 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/never.ts @@ -0,0 +1,19 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const neverToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'never'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.never, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/null.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/null.ts new file mode 100644 index 0000000000..fdb2a5875e --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/null.ts @@ -0,0 +1,19 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const nullToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'null'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.null, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/number.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/number.ts new file mode 100644 index 0000000000..8754becc9a --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/number.ts @@ -0,0 +1,94 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import { numberParameter } from '../../shared/numbers'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const numberToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'integer' | 'number'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; + + if (typeof schema.const === 'number') { + // TODO: parser - handle bigint constants + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.number(schema.const)], + }); + return expression; + } + + let numberExpression = tsc.callExpression({ + functionName: isBigInt + ? tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.coerce, + }), + name: identifiers.bigint, + }) + : tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.number, + }), + }); + + if (!isBigInt && schema.type === 'integer') { + numberExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: numberExpression, + name: identifiers.int, + }), + }); + } + + if (schema.exclusiveMinimum !== undefined) { + numberExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: numberExpression, + name: identifiers.gt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMinimum }), + ], + }); + } else if (schema.minimum !== undefined) { + numberExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: numberExpression, + name: identifiers.gte, + }), + parameters: [numberParameter({ isBigInt, value: schema.minimum })], + }); + } + + if (schema.exclusiveMaximum !== undefined) { + numberExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: numberExpression, + name: identifiers.lt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMaximum }), + ], + }); + } else if (schema.maximum !== undefined) { + numberExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: numberExpression, + name: identifiers.lte, + }), + parameters: [numberParameter({ isBigInt, value: schema.maximum })], + }); + } + + return numberExpression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/object.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/object.ts new file mode 100644 index 0000000000..748f4c33ad --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/object.ts @@ -0,0 +1,112 @@ +import ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import { numberRegExp } from '../../../../utils/regexp'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const objectToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'object'>; +}): Omit & { + anyType?: string; +} => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + let hasLazyExpression = false; + + // TODO: parser - handle constants + const properties: Array = []; + + const required = schema.required ?? []; + + for (const name in schema.properties) { + const property = schema.properties[name]!; + const isRequired = required.includes(name); + + const propertyExpression = irSchemaToAst({ + optional: !isRequired, + plugin, + schema: property, + state: { + ...state, + _path: toRef([...state._path.value, 'properties', name]), + }, + }); + + if (propertyExpression.hasLazyExpression) { + hasLazyExpression = true; + } + + numberRegExp.lastIndex = 0; + let propertyName; + if (numberRegExp.test(name)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = name.startsWith('-') + ? ts.factory.createStringLiteral(name) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = 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( + tsc.propertyAssignment({ + initializer: propertyExpression.expression, + name: propertyName, + }), + ); + } + + if ( + schema.additionalProperties && + (!schema.properties || !Object.keys(schema.properties).length) + ) { + const additionalAst = irSchemaToAst({ + plugin, + schema: schema.additionalProperties, + state: { + ...state, + _path: toRef([...state._path.value, 'additionalProperties']), + }, + }); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.record, + }), + parameters: [additionalAst.expression], + }); + return { + anyType: 'AnyZodObject', + expression, + hasLazyExpression: additionalAst.hasLazyExpression, + }; + } + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.object, + }), + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], + }); + return { + anyType: 'AnyZodObject', + expression, + hasLazyExpression, + }; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts new file mode 100644 index 0000000000..1512b1ed2b --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts @@ -0,0 +1,152 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const stringToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'string'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (typeof schema.const === 'string') { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.string(schema.const)], + }); + return expression; + } + + let stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.string, + }), + }); + + const dateTimeOptions: { key: string; value: boolean }[] = []; + + if (plugin.config.dates.offset) { + dateTimeOptions.push({ key: 'offset', value: true }); + } + if (plugin.config.dates.local) { + dateTimeOptions.push({ key: 'local', value: true }); + } + + if (schema.format) { + switch (schema.format) { + case 'date': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.date, + }), + }); + break; + case 'date-time': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.datetime, + }), + parameters: + dateTimeOptions.length > 0 + ? [ + tsc.objectExpression({ + obj: dateTimeOptions, + }), + ] + : [], + }); + break; + case 'email': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.email, + }), + }); + break; + case 'ipv4': + case 'ipv6': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.ip, + }), + }); + break; + case 'time': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.time, + }), + }); + break; + case 'uri': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.url, + }), + }); + break; + case 'uuid': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.uuid, + }), + }); + break; + } + } + + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + } else { + if (schema.minLength !== undefined) { + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.min, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + } + + if (schema.maxLength !== undefined) { + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.max, + }), + parameters: [tsc.valueToExpression({ value: schema.maxLength })], + }); + } + } + + if (schema.pattern) { + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.regex, + }), + parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], + }); + } + + return stringExpression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/tuple.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/tuple.ts new file mode 100644 index 0000000000..1bbcc1b548 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/tuple.ts @@ -0,0 +1,84 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const tupleToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'tuple'>; +}): Omit & { + anyType?: string; +} => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + let hasLazyExpression = false; + + if (schema.const && Array.isArray(schema.const)) { + const tupleElements = schema.const.map((value) => + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.valueToExpression({ value })], + }), + ); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return { + expression, + hasLazyExpression, + }; + } + + const tupleElements: Array = []; + + if (schema.items) { + schema.items.forEach((item, index) => { + const itemSchema = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + tupleElements.push(itemSchema.expression); + if (itemSchema.hasLazyExpression) { + hasLazyExpression = true; + } + }); + } + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return { + expression, + hasLazyExpression, + }; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/undefined.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/undefined.ts new file mode 100644 index 0000000000..f6d75fa79b --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/undefined.ts @@ -0,0 +1,19 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const undefinedToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'undefined'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.undefined, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/unknown.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/unknown.ts new file mode 100644 index 0000000000..4aa9a42743 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/unknown.ts @@ -0,0 +1,19 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const unknownToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'unknown'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.unknown, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/void.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/void.ts new file mode 100644 index 0000000000..fe0cb9e51b --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/void.ts @@ -0,0 +1,19 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const voidToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'void'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.void, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/api.ts b/packages/openapi-ts/src/plugins/zod/v4/api.ts new file mode 100644 index 0000000000..9de07623fc --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/api.ts @@ -0,0 +1,71 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../tsc'; +import { identifiers } from '../constants'; +import type { ValidatorArgs } from '../shared/types'; + +export const createRequestValidatorV4 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol(plugin.api.selector('data', operation.id)); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; + +export const createResponseValidatorV4 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol( + plugin.api.selector('responses', operation.id), + ); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/plugin.ts b/packages/openapi-ts/src/plugins/zod/v4/plugin.ts index 27ce9fe2bc..ccb222667d 100644 --- a/packages/openapi-ts/src/plugins/zod/v4/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/v4/plugin.ts @@ -1,1084 +1,84 @@ -import ts from 'typescript'; - import { deduplicateSchema } from '../../../ir/schema'; import type { IR } from '../../../ir/types'; import { buildName } from '../../../openApi/shared/utils/name'; import { tsc } from '../../../tsc'; import { refToName } from '../../../utils/ref'; -import { numberRegExp } from '../../../utils/regexp'; +import type { SchemaWithType } from '../../shared/types/schema'; +import { pathToSymbolResourceType } from '../../shared/utils/meta'; +import { toRef, toRefs } from '../../shared/utils/refs'; import { identifiers } from '../constants'; -import { exportZodSchema } from '../export'; +import { exportAst } from '../shared/export'; import { getZodModule } from '../shared/module'; -import { operationToZodSchema } from '../shared/operation'; -import type { SchemaWithType, State, ZodSchema } from '../shared/types'; -import { webhookToZodSchema } from '../shared/webhook'; +import { numberParameter } from '../shared/numbers'; +import { irOperationToAst } from '../shared/operation'; +import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; +import { irWebhookToAst } from '../shared/webhook'; import type { ZodPlugin } from '../types'; +import { irSchemaWithTypeToAst } from './toAst'; -const arrayTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'array'>; - state: State; -}): Omit => { - const result: Partial> = {}; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const functionName = tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }); - - if (!schema.items) { - result.expression = tsc.callExpression({ - functionName, - parameters: [ - unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }).expression, - ], - }); - } else { - schema = deduplicateSchema({ schema }); - - // at least one item is guaranteed - const itemExpressions = schema.items!.map((item) => { - const zodSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - if (zodSchema.hasCircularReference) { - result.hasCircularReference = true; - } - return zodSchema.expression; - }); - - if (itemExpressions.length === 1) { - result.expression = tsc.callExpression({ - functionName, - parameters: itemExpressions, - }); - } else { - 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 - // `.and()` as that does not exist on `.union()` and non-object schemas. - let intersectionExpression: ts.Expression; - if ( - firstSchema.logicalOperator === 'or' || - (firstSchema.type && firstSchema.type !== 'object') - ) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.intersection, - }), - parameters: itemExpressions, - }); - } else { - intersectionExpression = itemExpressions[0]!; - for (let i = 1; i < itemExpressions.length; i++) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: intersectionExpression, - name: identifiers.and, - }), - parameters: [itemExpressions[i]!], - }); - } - } - - result.expression = tsc.callExpression({ - functionName, - parameters: [intersectionExpression], - }); - } else { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemExpressions, - }), - ], - }), - ], - }); - } - } - } - - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - } else { - if (schema.minItems !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.min, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - } - - if (schema.maxItems !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.max, - }), - parameters: [tsc.valueToExpression({ value: schema.maxItems })], - }); - } - } - - return result as Omit; -}; - -const booleanTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'boolean'>; -}): Omit => { - const result: Partial> = {}; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (typeof schema.const === 'boolean') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.boolean, - }), - }); - return result as Omit; -}; - -const enumTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'enum'>; -}): Omit => { - const result: Partial> = {}; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const enumMembers: Array = []; - const literalMembers: Array = []; - - let isNullable = false; - let allStrings = true; - - for (const item of schema.items ?? []) { - // Zod supports string, number, and boolean enums - if (item.type === 'string' && typeof item.const === 'string') { - const stringLiteral = tsc.stringLiteral({ - text: item.const, - }); - enumMembers.push(stringLiteral); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [stringLiteral], - }), - ); - } else if ( - (item.type === 'number' || item.type === 'integer') && - typeof item.const === 'number' - ) { - allStrings = false; - const numberLiteral = tsc.ots.number(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [numberLiteral], - }), - ); - } else if (item.type === 'boolean' && typeof item.const === 'boolean') { - allStrings = false; - const booleanLiteral = tsc.ots.boolean(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [booleanLiteral], - }), - ); - } else if (item.type === 'null' || item.const === null) { - isNullable = true; - } - } - - if (!literalMembers.length) { - return unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }); - } - - // Use z.enum() for pure string enums, z.union() for mixed or non-string types - if (allStrings && enumMembers.length > 0) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.enum, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: enumMembers, - multiLine: false, - }), - ], - }); - } else if (literalMembers.length === 1) { - // For single-member unions, use the member directly instead of wrapping in z.union() - result.expression = literalMembers[0]; - } else { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: literalMembers, - multiLine: literalMembers.length > 3, - }), - ], - }); - } - - if (isNullable) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.nullable, - }), - parameters: [result.expression], - }); - } - - return result as Omit; -}; - -const neverTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'never'>; -}): Omit => { - const result: Partial> = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.never, - }), - }); - return result as Omit; -}; - -const nullTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'null'>; -}): Omit => { - const result: Partial> = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.null, - }), - }); - return result as Omit; -}; - -const numberParameter = ({ - isBigInt, - value, -}: { - isBigInt: boolean; - value: unknown; -}): ts.Expression | undefined => { - const expression = tsc.valueToExpression({ value }); - - if ( - isBigInt && - (typeof value === 'bigint' || - typeof value === 'number' || - typeof value === 'string' || - typeof value === 'boolean') - ) { - return tsc.callExpression({ - functionName: 'BigInt', - parameters: [expression], - }); - } - - return expression; -}; - -const numberTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'integer' | 'number'>; -}): Omit => { - const result: Partial> = {}; - - const isBigInt = schema.type === 'integer' && schema.format === 'int64'; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (typeof schema.const === 'number') { - // TODO: parser - handle bigint constants - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.number(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: isBigInt - ? tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.coerce, - }), - name: identifiers.bigint, - }) - : tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.number, - }), - }); - - if (!isBigInt && schema.type === 'integer') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.int, - }), - }); - } - - if (schema.exclusiveMinimum !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.gt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMinimum }), - ], - }); - } else if (schema.minimum !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.gte, - }), - parameters: [numberParameter({ isBigInt, value: schema.minimum })], - }); - } - - if (schema.exclusiveMaximum !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.lt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMaximum }), - ], - }); - } else if (schema.maximum !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.lte, - }), - parameters: [numberParameter({ isBigInt, value: schema.maximum })], - }); - } - - return result as Omit; -}; - -const objectTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'object'>; - state: State; -}): Omit => { - const result: Partial> = {}; - - // TODO: parser - handle constants - const properties: Array = - []; - - const required = schema.required ?? []; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - for (const name in schema.properties) { - const property = schema.properties[name]!; - const isRequired = required.includes(name); - - const propertySchema = schemaToZodSchema({ - optional: !isRequired, - plugin, - schema: property, - state, - }); - if (propertySchema.hasCircularReference) { - result.hasCircularReference = true; - } - - numberRegExp.lastIndex = 0; - let propertyName; - if (numberRegExp.test(name)) { - // For numeric literals, we'll handle negative numbers by using a string literal - // instead of trying to use a PrefixUnaryExpression - propertyName = name.startsWith('-') - ? ts.factory.createStringLiteral(name) - : ts.factory.createNumericLiteral(name); - } else { - propertyName = 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}'`; - } - - if (propertySchema.hasCircularReference) { - properties.push( - tsc.getAccessorDeclaration({ - name: propertyName, - // @ts-expect-error - returnType: propertySchema.typeName - ? tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: propertySchema.typeName, - }) - : undefined, - statements: [ - tsc.returnStatement({ - expression: propertySchema.expression, - }), - ], - }), - ); - } else { - properties.push( - tsc.propertyAssignment({ - initializer: propertySchema.expression, - name: propertyName, - }), - ); - } - } - - if ( - schema.additionalProperties && - (!schema.properties || !Object.keys(schema.properties).length) - ) { - const zodSchema = schemaToZodSchema({ - plugin, - schema: schema.additionalProperties, - state, - }); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.record, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.string, - }), - parameters: [], - }), - zodSchema.expression, - ], - }); - if (zodSchema.hasCircularReference) { - result.hasCircularReference = true; - } - - // Return with typeName for circular references - if (result.hasCircularReference) { - return { - ...result, - typeName: 'ZodType', - } as ZodSchema; - } - - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.object, - }), - parameters: [ts.factory.createObjectLiteralExpression(properties, true)], - }); - - // Return with typeName for circular references (AnyZodObject doesn't exist in Zod v4, use ZodType) - if (result.hasCircularReference) { - return { - ...result, - typeName: 'ZodType', - } as ZodSchema; - } - - return result as Omit; -}; - -const stringTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'string'>; -}): Omit => { - const result: Partial> = {}; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (typeof schema.const === 'string') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.string, - }), - }); - - const dateTimeOptions: { key: string; value: boolean }[] = []; - - if (plugin.config.dates.offset) { - dateTimeOptions.push({ key: 'offset', value: true }); - } - if (plugin.config.dates.local) { - dateTimeOptions.push({ key: 'local', value: true }); - } - - if (schema.format) { - switch (schema.format) { - case 'date': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.date, - }), - }); - break; - case 'date-time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.datetime, - }), - parameters: - dateTimeOptions.length > 0 - ? [ - tsc.objectExpression({ - obj: dateTimeOptions, - }), - ] - : [], - }); - break; - case 'email': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.email, - }), - }); - break; - case 'ipv4': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.ipv4, - }), - }); - break; - case 'ipv6': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.ipv6, - }), - }); - break; - case 'time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.time, - }), - }); - break; - case 'uri': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.url, - }), - }); - break; - case 'uuid': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.uuid, - }), - }); - break; - } - } - - if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - } else { - if (schema.minLength !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.min, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - } - - if (schema.maxLength !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.max, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }); - } - } - - if (schema.pattern) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }); - } - - return result as Omit; -}; - -const tupleTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'tuple'>; - state: State; -}): Omit => { - const result: Partial> = {}; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (schema.const && Array.isArray(schema.const)) { - const tupleElements = schema.const.map((value) => - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.valueToExpression({ value })], - }), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return result as Omit; - } - - const tupleElements: Array = []; - - for (const item of schema.items ?? []) { - const itemSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - tupleElements.push(itemSchema.expression); - - if (itemSchema.hasCircularReference) { - result.hasCircularReference = true; - } - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - - return result as Omit; -}; - -const undefinedTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'undefined'>; -}): Omit => { - const result: Partial> = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.undefined, - }), - }); - return result as Omit; -}; - -const unknownTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'unknown'>; -}): Omit => { - const result: Partial> = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.unknown, - }), - }); - return result as Omit; -}; - -const voidTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'void'>; -}): Omit => { - const result: Partial> = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.void, - }), - }); - return result as Omit; -}; - -const schemaTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: IR.SchemaObject; - state: State; -}): Omit => { - switch (schema.type as Required['type']) { - case 'array': - return arrayTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'array'>, - state, - }); - case 'boolean': - return booleanTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'boolean'>, - }); - case 'enum': - return enumTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'enum'>, - }); - case 'integer': - case 'number': - return numberTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'integer' | 'number'>, - }); - case 'never': - return neverTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'never'>, - }); - case 'null': - return nullTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'null'>, - }); - case 'object': - return objectTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'object'>, - state, - }); - case 'string': - return stringTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'string'>, - }); - case 'tuple': - return tupleTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'tuple'>, - state, - }); - case 'undefined': - return undefinedTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'undefined'>, - }); - case 'unknown': - return unknownTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'unknown'>, - }); - case 'void': - return voidTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'void'>, - }); - } -}; - -const schemaToZodSchema = ({ +export const irSchemaToAst = ({ optional, plugin, schema, state, -}: { +}: IrSchemaToAstOptions & { /** * Accept `optional` to handle optional object properties. We can't handle * this inside the object function because `.optional()` must come before * `.default()` which is handled in this function. */ optional?: boolean; - plugin: ZodPlugin['Instance']; schema: IR.SchemaObject; - state: State; -}): ZodSchema => { - let zodSchema: Partial = {}; +}): Ast => { + let ast: Partial = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); if (schema.$ref) { - const isCircularReference = state.circularReferenceTracker.includes( - schema.$ref, - ); - const isSelfReference = state.currentReferenceTracker.includes(schema.$ref); - state.circularReferenceTracker.push(schema.$ref); - state.currentReferenceTracker.push(schema.$ref); - - const selector = plugin.api.getSelector('ref', schema.$ref); - let symbol = plugin.getSymbol(selector); - - if (isCircularReference) { - if (!symbol) { - symbol = plugin.referenceSymbol(selector); - } - - if (isSelfReference) { - zodSchema.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.lazy, - }), - parameters: [ - tsc.arrowFunction({ - returnType: tsc.keywordTypeNode({ keyword: 'any' }), - statements: [ - tsc.returnStatement({ - expression: tsc.identifier({ text: symbol.placeholder }), - }), - ], - }), - ], - }); - } else { - zodSchema.expression = tsc.identifier({ text: symbol.placeholder }); - } - zodSchema.hasCircularReference = schema.circular; + const selector = plugin.api.selector('ref', schema.$ref); + const refSymbol = plugin.referenceSymbol(selector); + if (plugin.isSymbolRegistered(selector)) { + const ref = tsc.identifier({ text: refSymbol.placeholder }); + ast.expression = ref; } else { - if (!symbol) { - // if $ref hasn't been processed yet, inline it to avoid the - // "Block-scoped variable used before its declaration." error - // this could be (maybe?) fixed by reshuffling the generation order - const ref = plugin.context.resolveIrRef(schema.$ref); - handleComponent({ - id: schema.$ref, - plugin, - schema: ref, - state, - }); - } else { - zodSchema.hasCircularReference = schema.circular; - } - - const refSymbol = plugin.referenceSymbol(selector); - zodSchema.expression = tsc.identifier({ text: refSymbol.placeholder }); + const lazyExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.lazy, + }), + parameters: [ + tsc.arrowFunction({ + returnType: tsc.keywordTypeNode({ keyword: 'any' }), + statements: [ + tsc.returnStatement({ + expression: tsc.identifier({ text: refSymbol.placeholder }), + }), + ], + }), + ], + }); + ast.expression = lazyExpression; + ast.hasLazyExpression = true; + state.hasLazyExpression.value = true; } - - state.circularReferenceTracker.pop(); - state.currentReferenceTracker.pop(); } else if (schema.type) { - const zSchema = schemaTypeToZodSchema({ plugin, schema, state }); - zodSchema.expression = zSchema.expression; - zodSchema.hasCircularReference = zSchema.hasCircularReference; + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: schema as SchemaWithType, + state, + }); + ast.expression = typeAst.expression; + ast.hasLazyExpression = typeAst.hasLazyExpression; if (plugin.config.metadata && schema.description) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.register, }), parameters: [ tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.globalRegistry, }), tsc.objectExpression({ @@ -1096,11 +96,14 @@ const schemaToZodSchema = ({ schema = deduplicateSchema({ schema }); if (schema.items) { - const itemSchemas = schema.items.map((item) => - schemaToZodSchema({ + const itemSchemas = schema.items.map((item, index) => + irSchemaToAst({ plugin, schema: item, - state, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, }), ); @@ -1113,26 +116,26 @@ const schemaToZodSchema = ({ firstSchema.logicalOperator === 'or' || (firstSchema.type && firstSchema.type !== 'object') ) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.intersection, }), parameters: itemSchemas.map((schema) => schema.expression), }); } else { - zodSchema.expression = itemSchemas[0]!.expression; + ast.expression = itemSchemas[0]!.expression; itemSchemas.slice(1).forEach((schema) => { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression!, + expression: ast.expression!, name: identifiers.and, }), parameters: [ - schema.hasCircularReference + schema.hasLazyExpression ? tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.lazy, }), parameters: [ @@ -1151,9 +154,9 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.union, }), parameters: [ @@ -1164,39 +167,43 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema = schemaToZodSchema({ plugin, schema, state }); + ast = irSchemaToAst({ + plugin, + schema, + state, + }); } } else { // catch-all fallback for failed schemas - const zSchema = schemaTypeToZodSchema({ + const typeAst = irSchemaWithTypeToAst({ plugin, schema: { type: 'unknown', }, state, }); - zodSchema.expression = zSchema.expression; + ast.expression = typeAst.expression; } - if (zodSchema.expression) { + if (ast.expression) { if (schema.accessScope === 'read') { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.readonly, }), }); } if (optional) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.optional, }), - parameters: [zodSchema.expression], + parameters: [ast.expression], }); - zodSchema.typeName = identifiers.ZodOptional; + ast.typeName = identifiers.ZodOptional; } if (schema.default !== undefined) { @@ -1206,9 +213,9 @@ const schemaToZodSchema = ({ value: schema.default, }); if (callParameter) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.default, }), parameters: [callParameter], @@ -1217,60 +224,52 @@ const schemaToZodSchema = ({ } } - return zodSchema as ZodSchema; + return ast as Ast; }; const handleComponent = ({ - id, + $ref, plugin, schema, - state: _state, -}: { - id: string; - plugin: ZodPlugin['Instance']; + state, +}: IrSchemaToAstOptions & { + $ref: string; schema: IR.SchemaObject; - state?: Omit; }): void => { - const state: State = { - circularReferenceTracker: [id], - hasCircularReference: false, - ..._state, - currentReferenceTracker: [id], - }; - - const selector = plugin.api.getSelector('ref', id); - let symbol = plugin.getSymbol(selector); - if (symbol && !plugin.getSymbolValue(symbol)) return; - - const zodSchema = schemaToZodSchema({ plugin, schema, state }); - const baseName = refToName(id); - symbol = plugin.registerSymbol({ + const ast = irSchemaToAst({ plugin, schema, state }); + const baseName = refToName($ref); + const resourceType = pathToSymbolResourceType(state._path.value); + const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.definitions, name: baseName, }), - selector, + selector: plugin.api.selector('ref', $ref), }); const typeInferSymbol = plugin.config.definitions.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.definitions.types.infer, name: baseName, }), - selector: plugin.api.getSelector('type-infer-ref', id), + selector: plugin.api.selector('type-infer-ref', $ref), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema, symbol, typeInferSymbol, - zodSchema, }); }; @@ -1278,7 +277,7 @@ export const handlerV4: ZodPlugin['Handler'] = ({ plugin }) => { plugin.registerSymbol({ external: getZodModule({ plugin }), name: 'z', - selector: plugin.api.getSelector('import', 'zod'), + selector: plugin.api.selector('external', 'zod.z'), }); plugin.forEach( @@ -1290,52 +289,68 @@ export const handlerV4: ZodPlugin['Handler'] = ({ plugin }) => { (event) => { switch (event.type) { case 'operation': - operationToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irOperationToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; case 'parameter': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.parameter.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'requestBody': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.requestBody.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'schema': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'webhook': - webhookToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irWebhookToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; } diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/array.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/array.ts new file mode 100644 index 0000000000..0b38482147 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/array.ts @@ -0,0 +1,155 @@ +import type ts from 'typescript'; + +import { deduplicateSchema } from '../../../../ir/schema'; +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; +import { unknownToAst } from './unknown'; + +export const arrayToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'array'>; +}): Omit => { + const result: Partial> = {}; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const functionName = tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }); + + if (!schema.items) { + result.expression = tsc.callExpression({ + functionName, + parameters: [ + unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }).expression, + ], + }); + } else { + schema = deduplicateSchema({ schema }); + + // at least one item is guaranteed + const itemExpressions = schema.items!.map((item, index) => { + const itemAst = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + if (itemAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + return itemAst.expression; + }); + + if (itemExpressions.length === 1) { + result.expression = tsc.callExpression({ + functionName, + parameters: itemExpressions, + }); + } else { + 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 + // `.and()` as that does not exist on `.union()` and non-object schemas. + let intersectionExpression: ts.Expression; + if ( + firstSchema.logicalOperator === 'or' || + (firstSchema.type && firstSchema.type !== 'object') + ) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.intersection, + }), + parameters: itemExpressions, + }); + } else { + intersectionExpression = itemExpressions[0]!; + for (let i = 1; i < itemExpressions.length; i++) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: intersectionExpression, + name: identifiers.and, + }), + parameters: [itemExpressions[i]!], + }); + } + } + + result.expression = tsc.callExpression({ + functionName, + parameters: [intersectionExpression], + }); + } else { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemExpressions, + }), + ], + }), + ], + }); + } + } + } + + if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + } else { + if (schema.minItems !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.min, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + } + + if (schema.maxItems !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.max, + }), + parameters: [tsc.valueToExpression({ value: schema.maxItems })], + }); + } + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts new file mode 100644 index 0000000000..ad4b0fbaea --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts @@ -0,0 +1,34 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const booleanToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'boolean'>; +}): Omit => { + const result: Partial> = {}; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (typeof schema.const === 'boolean') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.boolean(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.boolean, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts new file mode 100644 index 0000000000..e02d09ea7f --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts @@ -0,0 +1,127 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { unknownToAst } from './unknown'; + +export const enumToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'enum'>; +}): Omit => { + const result: Partial> = {}; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const enumMembers: Array = []; + const literalMembers: Array = []; + + let isNullable = false; + let allStrings = true; + + for (const item of schema.items ?? []) { + // Zod supports string, number, and boolean enums + if (item.type === 'string' && typeof item.const === 'string') { + const stringLiteral = tsc.stringLiteral({ + text: item.const, + }); + enumMembers.push(stringLiteral); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [stringLiteral], + }), + ); + } else if ( + (item.type === 'number' || item.type === 'integer') && + typeof item.const === 'number' + ) { + allStrings = false; + const numberLiteral = tsc.ots.number(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [numberLiteral], + }), + ); + } else if (item.type === 'boolean' && typeof item.const === 'boolean') { + allStrings = false; + const booleanLiteral = tsc.ots.boolean(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [booleanLiteral], + }), + ); + } else if (item.type === 'null' || item.const === null) { + isNullable = true; + } + } + + if (!literalMembers.length) { + return unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + } + + // Use z.enum() for pure string enums, z.union() for mixed or non-string types + if (allStrings && enumMembers.length > 0) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.enum, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: enumMembers, + multiLine: false, + }), + ], + }); + } else if (literalMembers.length === 1) { + // For single-member unions, use the member directly instead of wrapping in z.union() + result.expression = literalMembers[0]; + } else { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: literalMembers, + multiLine: literalMembers.length > 3, + }), + ], + }); + } + + if (isNullable) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.nullable, + }), + parameters: [result.expression], + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/index.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/index.ts new file mode 100644 index 0000000000..62c481bfd5 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/index.ts @@ -0,0 +1,85 @@ +import type { SchemaWithType } from '../../../shared/types/schema'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { arrayToAst } from './array'; +import { booleanToAst } from './boolean'; +import { enumToAst } from './enum'; +import { neverToAst } from './never'; +import { nullToAst } from './null'; +import { numberToAst } from './number'; +import { objectToAst } from './object'; +import { stringToAst } from './string'; +import { tupleToAst } from './tuple'; +import { undefinedToAst } from './undefined'; +import { unknownToAst } from './unknown'; +import { voidToAst } from './void'; + +export const irSchemaWithTypeToAst = ({ + schema, + ...args +}: IrSchemaToAstOptions & { + schema: SchemaWithType; +}): Omit => { + switch (schema.type) { + case 'array': + return arrayToAst({ + ...args, + schema: schema as SchemaWithType<'array'>, + }); + case 'boolean': + return booleanToAst({ + ...args, + schema: schema as SchemaWithType<'boolean'>, + }); + case 'enum': + return enumToAst({ + ...args, + schema: schema as SchemaWithType<'enum'>, + }); + case 'integer': + case 'number': + return numberToAst({ + ...args, + schema: schema as SchemaWithType<'integer' | 'number'>, + }); + case 'never': + return neverToAst({ + ...args, + schema: schema as SchemaWithType<'never'>, + }); + case 'null': + return nullToAst({ + ...args, + schema: schema as SchemaWithType<'null'>, + }); + case 'object': + return objectToAst({ + ...args, + schema: schema as SchemaWithType<'object'>, + }); + case 'string': + return stringToAst({ + ...args, + schema: schema as SchemaWithType<'string'>, + }); + case 'tuple': + return tupleToAst({ + ...args, + schema: schema as SchemaWithType<'tuple'>, + }); + case 'undefined': + return undefinedToAst({ + ...args, + schema: schema as SchemaWithType<'undefined'>, + }); + case 'unknown': + return unknownToAst({ + ...args, + schema: schema as SchemaWithType<'unknown'>, + }); + case 'void': + return voidToAst({ + ...args, + schema: schema as SchemaWithType<'void'>, + }); + } +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/never.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/never.ts new file mode 100644 index 0000000000..a473632406 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/never.ts @@ -0,0 +1,20 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const neverToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'never'>; +}): Omit => { + const result: Partial> = {}; + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.never, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/null.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/null.ts new file mode 100644 index 0000000000..725bfa73ba --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/null.ts @@ -0,0 +1,20 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const nullToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'null'>; +}): Omit => { + const result: Partial> = {}; + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.null, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/number.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/number.ts new file mode 100644 index 0000000000..0a5cc1364c --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/number.ts @@ -0,0 +1,96 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import { numberParameter } from '../../shared/numbers'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const numberToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'integer' | 'number'>; +}): Omit => { + const result: Partial> = {}; + + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (typeof schema.const === 'number') { + // TODO: parser - handle bigint constants + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.number(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: isBigInt + ? tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.coerce, + }), + name: identifiers.bigint, + }) + : tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.number, + }), + }); + + if (!isBigInt && schema.type === 'integer') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.int, + }), + }); + } + + if (schema.exclusiveMinimum !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.gt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMinimum }), + ], + }); + } else if (schema.minimum !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.gte, + }), + parameters: [numberParameter({ isBigInt, value: schema.minimum })], + }); + } + + if (schema.exclusiveMaximum !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.lt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMaximum }), + ], + }); + } else if (schema.maximum !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.lte, + }), + parameters: [numberParameter({ isBigInt, value: schema.maximum })], + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/object.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/object.ts new file mode 100644 index 0000000000..021840ea35 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/object.ts @@ -0,0 +1,146 @@ +import ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import { numberRegExp } from '../../../../utils/regexp'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const objectToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'object'>; +}): Omit => { + const result: Partial> = {}; + + // TODO: parser - handle constants + const properties: Array = + []; + + const required = schema.required ?? []; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + for (const name in schema.properties) { + const property = schema.properties[name]!; + const isRequired = required.includes(name); + + const propertyAst = irSchemaToAst({ + optional: !isRequired, + plugin, + schema: property, + state: { + ...state, + _path: toRef([...state._path.value, 'properties', name]), + }, + }); + if (propertyAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + + numberRegExp.lastIndex = 0; + let propertyName; + if (numberRegExp.test(name)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = name.startsWith('-') + ? ts.factory.createStringLiteral(name) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = 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}'`; + } + + if (propertyAst.hasLazyExpression) { + properties.push( + tsc.getAccessorDeclaration({ + name: propertyName, + statements: [ + tsc.returnStatement({ + expression: propertyAst.expression, + }), + ], + }), + ); + } else { + properties.push( + tsc.propertyAssignment({ + initializer: propertyAst.expression, + name: propertyName, + }), + ); + } + } + + if ( + schema.additionalProperties && + (!schema.properties || !Object.keys(schema.properties).length) + ) { + const additionalAst = irSchemaToAst({ + plugin, + schema: schema.additionalProperties, + state: { + ...state, + _path: toRef([...state._path.value, 'additionalProperties']), + }, + }); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.record, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.string, + }), + parameters: [], + }), + additionalAst.expression, + ], + }); + if (additionalAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + + // Return with typeName for circular references + if (result.hasLazyExpression) { + return { + ...result, + typeName: 'ZodType', + } as Ast; + } + + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.object, + }), + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], + }); + + // Return with typeName for circular references (AnyZodObject doesn't exist in Zod v4, use ZodType) + if (result.hasLazyExpression) { + return { + ...result, + typeName: 'ZodType', + } as Ast; + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts new file mode 100644 index 0000000000..e6a2c71ab2 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts @@ -0,0 +1,170 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const stringToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'string'>; +}): Omit => { + const result: Partial> = {}; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (typeof schema.const === 'string') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.string(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.string, + }), + }); + + const dateTimeOptions: { key: string; value: boolean }[] = []; + + if (plugin.config.dates.offset) { + dateTimeOptions.push({ key: 'offset', value: true }); + } + if (plugin.config.dates.local) { + dateTimeOptions.push({ key: 'local', value: true }); + } + + if (schema.format) { + switch (schema.format) { + case 'date': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.date, + }), + }); + break; + case 'date-time': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.datetime, + }), + parameters: + dateTimeOptions.length > 0 + ? [ + tsc.objectExpression({ + obj: dateTimeOptions, + }), + ] + : [], + }); + break; + case 'email': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.email, + }), + }); + break; + case 'ipv4': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.ipv4, + }), + }); + break; + case 'ipv6': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.ipv6, + }), + }); + break; + case 'time': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.time, + }), + }); + break; + case 'uri': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.url, + }), + }); + break; + case 'uuid': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.uuid, + }), + }); + break; + } + } + + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + } else { + if (schema.minLength !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.min, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + } + + if (schema.maxLength !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.max, + }), + parameters: [tsc.valueToExpression({ value: schema.maxLength })], + }); + } + } + + if (schema.pattern) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.regex, + }), + parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/tuple.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/tuple.ts new file mode 100644 index 0000000000..f4c35f9a59 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/tuple.ts @@ -0,0 +1,77 @@ +import type ts from 'typescript'; + +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { toRef } from '../../../shared/utils/refs'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const tupleToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'tuple'>; +}): Omit => { + const result: Partial> = {}; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (schema.const && Array.isArray(schema.const)) { + const tupleElements = schema.const.map((value) => + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.valueToExpression({ value })], + }), + ); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return result as Omit; + } + + const tupleElements: Array = []; + + if (schema.items) { + schema.items.forEach((item, index) => { + const itemSchema = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + tupleElements.push(itemSchema.expression); + if (itemSchema.hasLazyExpression) { + result.hasLazyExpression = true; + } + }); + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/undefined.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/undefined.ts new file mode 100644 index 0000000000..294fba4834 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/undefined.ts @@ -0,0 +1,20 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const undefinedToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'undefined'>; +}): Omit => { + const result: Partial> = {}; + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.undefined, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/unknown.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/unknown.ts new file mode 100644 index 0000000000..7f1163aecf --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/unknown.ts @@ -0,0 +1,20 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const unknownToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'unknown'>; +}): Omit => { + const result: Partial> = {}; + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.unknown, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/void.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/void.ts new file mode 100644 index 0000000000..63324d5038 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/void.ts @@ -0,0 +1,20 @@ +import { tsc } from '../../../../tsc'; +import type { SchemaWithType } from '../../../shared/types/schema'; +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const voidToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'void'>; +}): Omit => { + const result: Partial> = {}; + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.void, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts b/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts index 79cbf7c053..e9d378053e 100644 --- a/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts +++ b/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts @@ -82,7 +82,7 @@ describe('registerHandlebarHelpers', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -94,7 +94,7 @@ describe('registerHandlebarHelpers', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -105,8 +105,8 @@ describe('registerHandlebarHelpers', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', @@ -213,7 +213,7 @@ describe('registerHandlebarTemplates', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -225,7 +225,7 @@ describe('registerHandlebarTemplates', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -236,8 +236,8 @@ describe('registerHandlebarTemplates', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/utils/__tests__/minHeap.test.ts b/packages/openapi-ts/src/utils/__tests__/minHeap.test.ts new file mode 100644 index 0000000000..63fb3bcd17 --- /dev/null +++ b/packages/openapi-ts/src/utils/__tests__/minHeap.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; + +import { MinHeap } from '../minHeap'; + +describe('MinHeap', () => { + it('pops items in increasing declIndex order', () => { + const idx = new Map([ + ['a', 10], + ['b', 5], + ['c', 20], + ]); + const h = new MinHeap(idx); + h.push('a'); + h.push('b'); + h.push('c'); + + expect(h.pop()).toBe('b'); + expect(h.pop()).toBe('a'); + expect(h.pop()).toBe('c'); + expect(h.pop()).toBeUndefined(); + }); + + it('supports interleaved push/pop and maintains order', () => { + const idx = new Map([ + ['x', 0], + ['y', 1], + ['z', 2], + ]); + const h = new MinHeap(idx); + h.push('y'); + expect(h.pop()).toBe('y'); + + h.push('z'); + h.push('x'); + expect(h.pop()).toBe('x'); + expect(h.pop()).toBe('z'); + }); + + it('handles duplicates (same id pushed multiple times)', () => { + const idx = new Map([['dup', 1]]); + const h = new MinHeap(idx); + h.push('dup'); + h.push('dup'); + expect(h.pop()).toBe('dup'); + // second duplicate still present + expect(h.pop()).toBe('dup'); + expect(h.pop()).toBeUndefined(); + }); + + it('isEmpty returns true when empty and false when not', () => { + const idx = new Map([['a', 0]]); + const h = new MinHeap(idx); + expect(h.isEmpty()).toBe(true); + h.push('a'); + expect(h.isEmpty()).toBe(false); + h.pop(); + expect(h.isEmpty()).toBe(true); + }); +}); diff --git a/packages/openapi-ts/src/utils/__tests__/parse.test.ts b/packages/openapi-ts/src/utils/__tests__/parse.test.ts index 90cc82667f..129b476f5c 100644 --- a/packages/openapi-ts/src/utils/__tests__/parse.test.ts +++ b/packages/openapi-ts/src/utils/__tests__/parse.test.ts @@ -72,7 +72,7 @@ describe('operationNameFn', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -103,7 +103,7 @@ describe('operationNameFn', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -124,7 +124,7 @@ describe('operationNameFn', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -144,7 +144,7 @@ describe('operationNameFn', () => { plugins: { '@hey-api/client-fetch': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/client-fetch', @@ -157,7 +157,7 @@ describe('operationNameFn', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -177,7 +177,7 @@ describe('operationNameFn', () => { plugins: { '@hey-api/client-fetch': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/client-fetch', @@ -190,7 +190,7 @@ describe('operationNameFn', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', diff --git a/packages/openapi-ts/src/utils/escape.ts b/packages/openapi-ts/src/utils/escape.ts index 35728b37cc..cf0aa9392b 100644 --- a/packages/openapi-ts/src/utils/escape.ts +++ b/packages/openapi-ts/src/utils/escape.ts @@ -1,4 +1,4 @@ -import { EOL } from 'os'; +import { EOL } from 'node:os'; import { validTypescriptIdentifierRegExp } from './regexp'; diff --git a/packages/openapi-ts/src/utils/minHeap.ts b/packages/openapi-ts/src/utils/minHeap.ts new file mode 100644 index 0000000000..0c502a4117 --- /dev/null +++ b/packages/openapi-ts/src/utils/minHeap.ts @@ -0,0 +1,64 @@ +export class MinHeap { + private heap: Array = []; + + constructor(public declIndex: Map) {} + + isEmpty(): boolean { + return !this.heap.length; + } + + pop(): string | undefined { + const [top] = this.heap; + if (!this.heap.length) return; + const last = this.heap.pop()!; + if (!this.heap.length) return top; + this.heap[0] = last; + this.sinkDown(0); + return top; + } + + push(item: string): void { + this.heap.push(item); + this.bubbleUp(this.heap.length - 1); + } + + private bubbleUp(index: number): void { + const heap = this.heap; + while (index > 0) { + const parent = Math.floor((index - 1) / 2); + const parentVal = heap[parent]!; + const curVal = heap[index]!; + if (this.declIndex.get(parentVal)! <= this.declIndex.get(curVal)!) break; + heap[parent] = curVal; + heap[index] = parentVal; + index = parent; + } + } + + private sinkDown(index: number): void { + const heap = this.heap; + const len = heap.length; + while (true) { + const left = 2 * index + 1; + const right = 2 * index + 2; + let smallest = index; + if (left < len) { + const leftVal = heap[left]!; + const smallestVal = heap[smallest]!; + if (this.declIndex.get(leftVal)! < this.declIndex.get(smallestVal)!) + smallest = left; + } + if (right < len) { + const rightVal = heap[right]!; + const smallestVal = heap[smallest]!; + if (this.declIndex.get(rightVal)! < this.declIndex.get(smallestVal)!) + smallest = right; + } + if (smallest === index) break; + const tmp = heap[smallest]!; + heap[smallest] = heap[index]!; + heap[index] = tmp; + index = smallest; + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96f0c9abe0..d6b080f754 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1265,8 +1265,8 @@ importers: specifier: workspace:^0.3.0 version: link:../codegen-core '@hey-api/json-schema-ref-parser': - specifier: 1.2.0 - version: 1.2.0 + specifier: 1.2.1 + version: 1.2.1 ansi-colors: specifier: 4.1.3 version: 4.1.3 @@ -1427,6 +1427,9 @@ importers: '@types/cross-spawn': specifier: 6.0.6 version: 6.0.6 + arktype: + specifier: 2.1.23 + version: 2.1.23 axios: specifier: 1.8.2 version: 1.8.2 @@ -2124,6 +2127,15 @@ packages: resolution: {integrity: sha512-Izvir8iIoU+X4SKtDAa5kpb+9cpifclzsbA8x/AZY0k0gIfXYQ1fa1B6Epfe6vNA2YfDX8VtrZFgvnXB6aPEoQ==} engines: {node: '>=18'} + '@ark/regex@0.0.0': + resolution: {integrity: sha512-p4vsWnd/LRGOdGQglbwOguIVhPmCAf5UzquvnDoxqhhPWTP84wWgi1INea8MgJ4SnI2gp37f13oA4Waz9vwNYg==} + + '@ark/schema@0.50.0': + resolution: {integrity: sha512-hfmP82GltBZDadIOeR3argKNlYYyB2wyzHp0eeAqAOFBQguglMV/S7Ip2q007bRtKxIMLDqFY6tfPie1dtssaQ==} + + '@ark/util@0.50.0': + resolution: {integrity: sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -3947,8 +3959,8 @@ packages: '@fontsource/fira-mono@5.0.0': resolution: {integrity: sha512-IsinH/oLYJyv/sQv7SbKmjoAXZsSjm6Q1Tz5GBBXCXi3Jg9MzXmKvWm9bSLC8lFI6CDsi8GkH/DAgZ98t8bhTQ==} - '@hey-api/json-schema-ref-parser@1.2.0': - resolution: {integrity: sha512-BMnIuhVgNmSudadw1GcTsP18Yk5l8FrYrg/OSYNxz0D2E0vf4D5e4j5nUbuY8MU6p1vp7ev0xrfP6A/NWazkzQ==} + '@hey-api/json-schema-ref-parser@1.2.1': + resolution: {integrity: sha512-inPeksRLq+j3ArnuGOzQPQE//YrhezQG0+9Y9yizScBN2qatJ78fIByhEgKdNAbtguDCn4RPxmEhcrePwHxs4A==} engines: {node: '>= 16'} '@humanfs/core@0.19.1': @@ -7268,6 +7280,9 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + arktype@2.1.23: + resolution: {integrity: sha512-tyxNWX6xJVMb2EPJJ3OjgQS1G/vIeQRrZuY4DeBNQmh8n7geS+czgbauQWB6Pr+RXiOO8ChEey44XdmxsqGmfQ==} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -14220,7 +14235,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) - '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0))(webpack@5.98.0(esbuild@0.25.0)) + '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0)) '@angular-devkit/core': 19.2.0(chokidar@4.0.3) '@angular/build': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/platform-server@19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.0(@angular/animations@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))))(@angular/ssr@19.2.15(5c03da8199d2fcdf9ff93b70f9349edd))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.0) '@angular/compiler-cli': 19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3) @@ -14238,7 +14253,7 @@ snapshots: '@vitejs/plugin-basic-ssl': 1.2.0(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.2) - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) browserslist: 4.25.4 copy-webpack-plugin: 12.0.2(webpack@5.98.0) css-loader: 7.1.2(webpack@5.98.0) @@ -14258,7 +14273,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -14271,8 +14286,8 @@ snapshots: tslib: 2.8.1 typescript: 5.8.3 webpack: 5.98.0(esbuild@0.25.0) - webpack-dev-middleware: 7.4.2(webpack@5.98.0) - webpack-dev-server: 5.2.0(webpack@5.98.0) + webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.98.0) optionalDependencies: @@ -14308,7 +14323,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) - '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0))(webpack@5.98.0(esbuild@0.25.0)) + '@angular-devkit/build-webpack': 0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0)) '@angular-devkit/core': 19.2.0(chokidar@4.0.3) '@angular/build': 19.2.0(@angular/compiler-cli@19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/platform-server@19.2.0(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.0(@angular/animations@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))))(@angular/ssr@19.2.15(5c03da8199d2fcdf9ff93b70f9349edd))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.8.3)))(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.0) '@angular/compiler-cli': 19.2.0(@angular/compiler@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(typescript@5.8.3) @@ -14326,7 +14341,7 @@ snapshots: '@vitejs/plugin-basic-ssl': 1.2.0(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.2) - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) browserslist: 4.25.4 copy-webpack-plugin: 12.0.2(webpack@5.98.0) css-loader: 7.1.2(webpack@5.98.0) @@ -14346,7 +14361,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -14359,8 +14374,8 @@ snapshots: tslib: 2.8.1 typescript: 5.8.3 webpack: 5.98.0(esbuild@0.25.0) - webpack-dev-middleware: 7.4.2(webpack@5.98.0) - webpack-dev-server: 5.2.0(webpack@5.98.0) + webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) + webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.98.0) optionalDependencies: @@ -14434,7 +14449,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 @@ -14447,7 +14462,7 @@ snapshots: tslib: 2.8.1 typescript: 5.8.3 webpack: 5.98.0(esbuild@0.25.4) - webpack-dev-middleware: 7.4.2(webpack@5.98.0) + webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) webpack-dev-server: 5.2.2(webpack@5.98.0) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.98.0) @@ -14535,7 +14550,7 @@ snapshots: tslib: 2.8.1 typescript: 5.9.3 webpack: 5.98.0(esbuild@0.25.4) - webpack-dev-middleware: 7.4.2(webpack@5.98.0) + webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) webpack-dev-server: 5.2.2(webpack@5.98.0) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.98.0) @@ -14567,12 +14582,12 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-webpack@0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0))(webpack@5.98.0(esbuild@0.25.0))': + '@angular-devkit/build-webpack@0.1902.0(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)))(webpack@5.98.0(esbuild@0.25.0))': dependencies: '@angular-devkit/architect': 0.1902.0(chokidar@4.0.3) rxjs: 7.8.1 webpack: 5.98.0(esbuild@0.25.0) - webpack-dev-server: 5.2.0(webpack@5.98.0) + webpack-dev-server: 5.2.0(webpack@5.98.0(esbuild@0.25.0)) transitivePeerDependencies: - chokidar @@ -15182,6 +15197,16 @@ snapshots: typescript: 5.6.1-rc validate-npm-package-name: 5.0.1 + '@ark/regex@0.0.0': + dependencies: + '@ark/util': 0.50.0 + + '@ark/schema@0.50.0': + dependencies: + '@ark/util': 0.50.0 + + '@ark/util@0.50.0': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -17361,7 +17386,7 @@ snapshots: '@fontsource/fira-mono@5.0.0': {} - '@hey-api/json-schema-ref-parser@1.2.0': + '@hey-api/json-schema-ref-parser@1.2.1': dependencies: '@jsdevtools/ono': 7.1.3 '@types/json-schema': 7.0.15 @@ -21525,6 +21550,12 @@ snapshots: aria-query@5.3.2: {} + arktype@2.1.23: + dependencies: + '@ark/regex': 0.0.0 + '@ark/schema': 0.50.0 + '@ark/util': 0.50.0 + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -21697,7 +21728,7 @@ snapshots: schema-utils: 4.3.2 webpack: 5.98.0(esbuild@0.25.0) - babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)): + babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0): dependencies: '@babel/core': 7.26.9 find-cache-dir: 4.0.0 @@ -23130,7 +23161,7 @@ snapshots: eslint: 9.17.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.17.0(jiti@2.6.1)) @@ -23168,7 +23199,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -23183,7 +23214,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.17.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@9.17.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)))(eslint@9.17.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -26944,7 +26975,7 @@ snapshots: ts-node: 10.9.2(@types/node@22.10.5)(typescript@5.9.3) optional: true - postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)): + postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0): dependencies: cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.7 @@ -30140,7 +30171,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-dev-middleware@7.4.2(webpack@5.98.0): + webpack-dev-middleware@7.4.2(webpack@5.98.0(esbuild@0.25.0)): dependencies: colorette: 2.0.20 memfs: 4.38.2 @@ -30151,7 +30182,7 @@ snapshots: optionalDependencies: webpack: 5.98.0(esbuild@0.25.0) - webpack-dev-server@5.2.0(webpack@5.98.0): + webpack-dev-server@5.2.0(webpack@5.98.0(esbuild@0.25.0)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -30178,7 +30209,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.98.0) + webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) ws: 8.18.3 optionalDependencies: webpack: 5.98.0(esbuild@0.25.0) @@ -30216,7 +30247,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.98.0) + webpack-dev-middleware: 7.4.2(webpack@5.98.0(esbuild@0.25.0)) ws: 8.18.3 optionalDependencies: webpack: 5.98.0(esbuild@0.25.0)