From 62ab6d078bc5ace4d4f685517cb64bf68b20b900 Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Wed, 28 Jul 2021 22:24:20 +0000 Subject: [PATCH 1/9] Checks in test documenting behavior --- src/core/generate.test.ts | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/core/generate.test.ts b/src/core/generate.test.ts index 2ea2188..2a2778f 100644 --- a/src/core/generate.test.ts +++ b/src/core/generate.test.ts @@ -72,6 +72,54 @@ describe("generate", () => { " `); }); + it("should not have any errors", () => { + expect(errors.length).toBe(0); + }); + }); + + describe("with enums", () => { + const sourceText = ` + export enum Superhero { + Superman = "superman" + ClarkKent = "clark-kent" + }; + `; + + const { getZodSchemasFile, getIntegrationTestFile, errors } = generate({ + sourceText, + }); + + it("should generate the zod schemas", () => { + expect(getZodSchemasFile("./superhero")).toMatchInlineSnapshot(` + "// Generated by ts-to-zod + import { z } from \\"zod\\"; + import { Superhero } from \\"./superhero\\"; + + export const superheroSchema = z.nativeEnum(Superhero); + " + `); + }); + + it("should generate the integration tests", () => { + expect(getIntegrationTestFile("./superhero", "superhero.zod")) + .toMatchInlineSnapshot(` + "// Generated by ts-to-zod + import { z } from \\"zod\\"; + + import * as spec from \\"./superhero\\"; + import * as generated from \\"superhero.zod\\"; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function expectType(_: T) { + /* noop */ + } + + export type superheroSchemaInferredType = z.infer; + expectType({} as superheroSchemaInferredType) + expectType({} as spec.Superhero) + " + `); + }); it("should not have any errors", () => { expect(errors.length).toBe(0); From 67f78468e74ae402ac46ca4653935387e4fb2e3d Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Wed, 28 Jul 2021 22:26:22 +0000 Subject: [PATCH 2/9] Implements enum import & declartion --- src/core/generate.ts | 45 ++++++++++++++++++++++------------- src/core/generateZodSchema.ts | 10 +++++++- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/core/generate.ts b/src/core/generate.ts index 1a6bd00..533c7b5 100644 --- a/src/core/generate.ts +++ b/src/core/generate.ts @@ -53,10 +53,16 @@ export function generate({ ); // Extract the nodes (interface declarations & type aliases) - const nodes: Array = []; + const nodes: Array< + ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.EnumDeclaration + > = []; const visitor = (node: ts.Node) => { - if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) { + if ( + ts.isInterfaceDeclaration(node) || + ts.isTypeAliasDeclaration(node) || + ts.isEnumDeclaration(node) + ) { if (nameFilter(node.name.text)) { nodes.push(node); } @@ -91,23 +97,28 @@ export function generate({ while (statements.size !== zodSchemas.length && n < maxRun) { zodSchemas .filter(({ varName }) => !statements.has(varName)) - .forEach(({ varName, dependencies, statement, typeName }) => { - const isCircular = dependencies.includes(varName); - const missingDependencies = dependencies - .filter((dep) => dep !== varName) - .filter((dep) => !statements.has(dep)); - if (missingDependencies.length === 0) { - if (isCircular) { - typeImports.add(typeName); - statements.set(varName, { - value: transformRecursiveSchema("z", statement, typeName), - typeName, - }); - } else { - statements.set(varName, { value: statement, typeName }); + .forEach( + ({ varName, dependencies, statement, typeName, requiresImport }) => { + const isCircular = dependencies.includes(varName); + const missingDependencies = dependencies + .filter((dep) => dep !== varName) + .filter((dep) => !statements.has(dep)); + if (missingDependencies.length === 0) { + if (isCircular) { + typeImports.add(typeName); + statements.set(varName, { + value: transformRecursiveSchema("z", statement, typeName), + typeName, + }); + } else { + if (requiresImport) { + typeImports.add(typeName); + } + statements.set(varName, { value: statement, typeName }); + } } } - }); + ); n++; // Just a safety net to avoid infinity loops } diff --git a/src/core/generateZodSchema.ts b/src/core/generateZodSchema.ts index b0311f3..4b35e8b 100644 --- a/src/core/generateZodSchema.ts +++ b/src/core/generateZodSchema.ts @@ -19,7 +19,7 @@ export interface GenerateZodSchemaProps { /** * Interface or type node */ - node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration; + node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.EnumDeclaration; /** * Zod import value. @@ -57,6 +57,8 @@ export function generateZodSchemaVariableStatement({ }: GenerateZodSchemaProps) { let schema: ts.CallExpression | ts.Identifier | undefined; const dependencies: string[] = []; + let requiresImport = false; + // const imports: string[] = []; if (ts.isInterfaceDeclaration(node)) { let baseSchema: string | undefined; @@ -100,6 +102,11 @@ export function generateZodSchemaVariableStatement({ }); } + if (ts.isEnumDeclaration(node)) { + schema = buildZodSchema(zodImportValue, "nativeEnum", [node.name]); + requiresImport = true; + } + return { dependencies: uniq(dependencies), statement: f.createVariableStatement( @@ -116,6 +123,7 @@ export function generateZodSchemaVariableStatement({ ts.NodeFlags.Const ) ), + requiresImport, }; } From cec7bf29b86c33603d5c65bac52fb80d29d8c848 Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Wed, 28 Jul 2021 22:30:11 +0000 Subject: [PATCH 3/9] Updates example to include enum --- example/heros.ts | 10 ++++++++-- example/heros.zod.ts | 8 +++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/example/heros.ts b/example/heros.ts index 68639a8..34fb5bb 100644 --- a/example/heros.ts +++ b/example/heros.ts @@ -1,6 +1,12 @@ +export enum EnemyPower { + Flight = "flight", + Strength = "strength", + Speed = "speed", +} + export interface Enemy { name: string; - powers: string[]; + powers: EnemyPower[]; inPrison: boolean; } @@ -13,7 +19,7 @@ export interface Superman { export interface Villain { name: string; - powers: string[]; + powers: EnemyPower[]; friends: Villain[]; canBeTrusted: never; } diff --git a/example/heros.zod.ts b/example/heros.zod.ts index bb46b69..105c198 100644 --- a/example/heros.zod.ts +++ b/example/heros.zod.ts @@ -1,10 +1,12 @@ // Generated by ts-to-zod import { z } from "zod"; -import { Villain } from "./heros"; +import { EnemyPower, Villain } from "./heros"; + +export const enemyPowerSchema = z.nativeEnum(EnemyPower); export const enemySchema = z.object({ name: z.string(), - powers: z.array(z.string()), + powers: z.array(enemyPowerSchema), inPrison: z.boolean(), }); @@ -22,7 +24,7 @@ export const supermanSchema = z.object({ export const villainSchema: z.ZodSchema = z.lazy(() => z.object({ name: z.string(), - powers: z.array(z.string()), + powers: z.array(enemyPowerSchema), friends: z.array(villainSchema), canBeTrusted: z.never(), }) From ddfd772fbfb25f57348e16cb3b8bd89bf766b970 Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Thu, 29 Jul 2021 00:07:39 +0000 Subject: [PATCH 4/9] Updates tests to document expected case --- src/core/generate.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/core/generate.test.ts b/src/core/generate.test.ts index 2a2778f..8b2e386 100644 --- a/src/core/generate.test.ts +++ b/src/core/generate.test.ts @@ -83,6 +83,10 @@ describe("generate", () => { Superman = "superman" ClarkKent = "clark-kent" }; + + export type FavoriteSuperhero = { + superhero: Superhero.Superman + }; `; const { getZodSchemasFile, getIntegrationTestFile, errors } = generate({ @@ -96,6 +100,10 @@ describe("generate", () => { import { Superhero } from \\"./superhero\\"; export const superheroSchema = z.nativeEnum(Superhero); + + export const favoriteSuperheroSchema = z.object({ + superhero: z.literal(Superhero.Superman) + }); " `); }); @@ -115,8 +123,12 @@ describe("generate", () => { } export type superheroSchemaInferredType = z.infer; + + export type favoriteSuperheroSchemaInferredType = z.infer; expectType({} as superheroSchemaInferredType) expectType({} as spec.Superhero) + expectType({} as favoriteSuperheroSchemaInferredType) + expectType({} as spec.FavoriteSuperhero) " `); }); From 84aa7be29ed57b6d2ca9b787d0ec4b5e84be47db Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Thu, 29 Jul 2021 00:11:15 +0000 Subject: [PATCH 5/9] Add handler for enum literals --- src/core/generateZodSchema.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/core/generateZodSchema.ts b/src/core/generateZodSchema.ts index 4b35e8b..b224cfc 100644 --- a/src/core/generateZodSchema.ts +++ b/src/core/generateZodSchema.ts @@ -455,6 +455,25 @@ function buildZodPrimitive({ } } + // Deal with enums used as literals + if ( + ts.isTypeReferenceNode(typeNode) && + ts.isQualifiedName(typeNode.typeName) && + ts.isIdentifier(typeNode.typeName.left) + ) { + return buildZodSchema( + z, + "literal", + [ + f.createPropertyAccessExpression( + typeNode.typeName.left, + typeNode.typeName.right + ), + ], + zodProperties + ); + } + if (ts.isArrayTypeNode(typeNode)) { return buildZodSchema( z, From 814a5ce4566cce3e6c8af14a61bd0225a34430e8 Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Thu, 29 Jul 2021 00:11:39 +0000 Subject: [PATCH 6/9] Updates example --- example/heros.ts | 4 ++++ example/heros.zod.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/example/heros.ts b/example/heros.ts index 34fb5bb..d7d65bc 100644 --- a/example/heros.ts +++ b/example/heros.ts @@ -4,6 +4,10 @@ export enum EnemyPower { Speed = "speed", } +export type SpeedEnemy = { + power: EnemyPower.Speed; +}; + export interface Enemy { name: string; powers: EnemyPower[]; diff --git a/example/heros.zod.ts b/example/heros.zod.ts index 105c198..fe7bc1b 100644 --- a/example/heros.zod.ts +++ b/example/heros.zod.ts @@ -4,6 +4,10 @@ import { EnemyPower, Villain } from "./heros"; export const enemyPowerSchema = z.nativeEnum(EnemyPower); +export const speedEnemySchema = z.object({ + power: z.literal(EnemyPower.Speed), +}); + export const enemySchema = z.object({ name: z.string(), powers: z.array(enemyPowerSchema), From bf1a203c346c421bf472384b5151a4975561e1fc Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Thu, 29 Jul 2021 17:42:19 +0000 Subject: [PATCH 7/9] Addresses https://github.com/fabien0102/ts-to-zod/pull/38#discussion_r678935406 --- src/core/generateZodSchema.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/generateZodSchema.ts b/src/core/generateZodSchema.ts index b224cfc..cdd58ed 100644 --- a/src/core/generateZodSchema.ts +++ b/src/core/generateZodSchema.ts @@ -58,7 +58,6 @@ export function generateZodSchemaVariableStatement({ let schema: ts.CallExpression | ts.Identifier | undefined; const dependencies: string[] = []; let requiresImport = false; - // const imports: string[] = []; if (ts.isInterfaceDeclaration(node)) { let baseSchema: string | undefined; From 7de337204eb25d998d7e5d8be2b4a452589811c3 Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Thu, 29 Jul 2021 17:45:33 +0000 Subject: [PATCH 8/9] Addresses https://github.com/fabien0102/ts-to-zod/pull/38#issuecomment-888951792 --- src/core/generateZodSchema.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/core/generateZodSchema.test.ts b/src/core/generateZodSchema.test.ts index 9ea8d28..5de0b28 100644 --- a/src/core/generateZodSchema.test.ts +++ b/src/core/generateZodSchema.test.ts @@ -95,6 +95,16 @@ describe("generateZodSchema", () => { ); }); + it("should generate a nativeEnum schema", () => { + const source = `export enum Superhero = { + Superman = "superman", + ClarkKent = "clark_kent", + };`; + expect(generate(source)).toMatchInlineSnapshot( + `"export const superheroSchema = z.nativeEnum(Superhero);"` + ); + }); + it("should generate a never", () => { const source = `export type CanBeatZod = never;`; expect(generate(source)).toMatchInlineSnapshot( From d3e9caf2fc8a8ea8c1d7d67b3677ab530849f373 Mon Sep 17 00:00:00 2001 From: Brian Anglin Date: Thu, 29 Jul 2021 17:45:54 +0000 Subject: [PATCH 9/9] Addresses https://github.com/fabien0102/ts-to-zod/pull/40#discussion_r678971858 --- src/core/generateZodSchema.test.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/core/generateZodSchema.test.ts b/src/core/generateZodSchema.test.ts index 5de0b28..25064b7 100644 --- a/src/core/generateZodSchema.test.ts +++ b/src/core/generateZodSchema.test.ts @@ -95,6 +95,19 @@ describe("generateZodSchema", () => { ); }); + it("should generate a literal schema (enum)", () => { + const source = ` + export type BestSuperhero = { + superhero: Superhero.Superman + }; + `; + expect(generate(source)).toMatchInlineSnapshot(` + "export const bestSuperheroSchema = z.object({ + superhero: z.literal(Superhero.Superman) + });" + `); + }); + it("should generate a nativeEnum schema", () => { const source = `export enum Superhero = { Superman = "superman", @@ -630,8 +643,15 @@ function generate(sourceText: string, z?: string) { ); const declaration = findNode( sourceFile, - (node): node is ts.InterfaceDeclaration | ts.TypeAliasDeclaration => - ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) + ( + node + ): node is + | ts.InterfaceDeclaration + | ts.TypeAliasDeclaration + | ts.EnumDeclaration => + ts.isInterfaceDeclaration(node) || + ts.isTypeAliasDeclaration(node) || + ts.isEnumDeclaration(node) ); if (!declaration) { throw new Error("No `type` or `interface` found!");