From f66c172c9a3ca70c701afea5e3b9a5a44bc3eff7 Mon Sep 17 00:00:00 2001 From: Thomas V <2619415+tvillaren@users.noreply.github.com> Date: Sat, 29 Jun 2024 14:32:27 +0200 Subject: [PATCH 1/4] test: adding failing test case --- src/core/generateZodSchema.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/generateZodSchema.test.ts b/src/core/generateZodSchema.test.ts index 567da498..099aa04c 100644 --- a/src/core/generateZodSchema.test.ts +++ b/src/core/generateZodSchema.test.ts @@ -372,6 +372,15 @@ describe("generateZodSchema", () => { ); }); + it("should generate a schema with omit in interface extension clause", () => { + const source = `export interface Superman extends Omit { + withPower: boolean; + }`; + expect(generate(source)).toMatchInlineSnapshot( + `"export const supermanSchema = clarkSchema.omit({ "weakness": true }).extend({ withPower: z.boolean() });"` + ); + }); + it("should generate a schema with pick", () => { const source = `export type YouJustKnowMyName = Pick;`; expect(generate(source)).toMatchInlineSnapshot( From 45ab44d4bda46601c79b378fd905dab0d4023fa5 Mon Sep 17 00:00:00 2001 From: Thomas V <2619415+tvillaren@users.noreply.github.com> Date: Sat, 29 Jun 2024 14:34:23 +0200 Subject: [PATCH 2/4] partial: started refactoring for fix --- src/core/generateZodSchema.ts | 142 +++++++++++++++++++++------------- 1 file changed, 87 insertions(+), 55 deletions(-) diff --git a/src/core/generateZodSchema.ts b/src/core/generateZodSchema.ts index 039d3e74..bcfaa3c4 100644 --- a/src/core/generateZodSchema.ts +++ b/src/core/generateZodSchema.ts @@ -95,7 +95,17 @@ export function generateZodSchemaVariableStatement({ // Looping on types browses the comma-separated interfaces const heritages = h.types.map((expression) => { - return getDependencyName(expression.getText(sourceFile)); + const identifierName = expression.expression.getText(sourceFile); + + if ( + ["Omit", "Pick"].includes(identifierName) && + expression.typeArguments + ) { + // do something here + // probably need to review schemaExtensionClauses to include Pick/Omit handling + } + + return getDependencyName(identifierName); }); return deps.concat(heritages); @@ -515,60 +525,15 @@ function buildZodPrimitive({ // Deal with `Omit<>` & `Pick<>` syntax if (["Omit", "Pick"].includes(identifierName) && typeNode.typeArguments) { - const [originalType, keys] = typeNode.typeArguments; - let parameters: ts.ObjectLiteralExpression | undefined; - - if (ts.isLiteralTypeNode(keys)) { - parameters = f.createObjectLiteralExpression([ - f.createPropertyAssignment( - keys.literal.getText(sourceFile), - f.createTrue() - ), - ]); - } - if (ts.isUnionTypeNode(keys)) { - parameters = f.createObjectLiteralExpression( - keys.types.map((type) => { - if (!ts.isLiteralTypeNode(type)) { - throw new Error( - `${identifierName} unknown syntax: (${ - ts.SyntaxKind[type.kind] - } as K union part not supported)` - ); - } - return f.createPropertyAssignment( - type.literal.getText(sourceFile), - f.createTrue() - ); - }) - ); - } - - if (!parameters) { - throw new Error( - `${identifierName} unknown syntax: (${ - ts.SyntaxKind[keys.kind] - } as K not supported)` - ); - } - - return f.createCallExpression( - f.createPropertyAccessExpression( - buildZodPrimitive({ - z, - typeNode: originalType, - isOptional: false, - jsDocTags: {}, - sourceFile, - dependencies, - getDependencyName, - skipParseJSDoc, - customJSDocFormatTypes, - }), - f.createIdentifier(lower(identifierName)) - ), - undefined, - [parameters] + return buildOmitPickObject( + identifierName, + typeNode.typeArguments, + z, + sourceFile, + dependencies, + getDependencyName, + skipParseJSDoc, + customJSDocFormatTypes ); } @@ -1344,3 +1309,70 @@ function buildSchemaReference( throw new Error("Unknown IndexedAccessTypeNode.objectType type"); } + +function buildOmitPickObject( + identifierName: string, + typeArguments: ts.NodeArray, + z: string, + sourceFile: ts.SourceFile, + dependencies: string[], + getDependencyName: (id: string) => string, + skipParseJSDoc: boolean, + customJSDocFormatTypes: CustomJSDocFormatTypes +) { + const [originalType, keys] = typeArguments; + let parameters: ts.ObjectLiteralExpression | undefined; + + if (ts.isLiteralTypeNode(keys)) { + parameters = f.createObjectLiteralExpression([ + f.createPropertyAssignment( + keys.literal.getText(sourceFile), + f.createTrue() + ), + ]); + } + if (ts.isUnionTypeNode(keys)) { + parameters = f.createObjectLiteralExpression( + keys.types.map((type) => { + if (!ts.isLiteralTypeNode(type)) { + throw new Error( + `${identifierName} unknown syntax: (${ + ts.SyntaxKind[type.kind] + } as K union part not supported)` + ); + } + return f.createPropertyAssignment( + type.literal.getText(sourceFile), + f.createTrue() + ); + }) + ); + } + + if (!parameters) { + throw new Error( + `${identifierName} unknown syntax: (${ + ts.SyntaxKind[keys.kind] + } as K not supported)` + ); + } + + return f.createCallExpression( + f.createPropertyAccessExpression( + buildZodPrimitive({ + z, + typeNode: originalType, + isOptional: false, + jsDocTags: {}, + sourceFile, + dependencies, + getDependencyName, + skipParseJSDoc, + customJSDocFormatTypes, + }), + f.createIdentifier(lower(identifierName)) + ), + undefined, + [parameters] + ); +} From 341fee94c7b8a1d609b0490108d7e504b231e4f0 Mon Sep 17 00:00:00 2001 From: Thomas V <2619415+tvillaren@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:29:49 +0200 Subject: [PATCH 3/4] feat: make tests pass + refacto --- src/core/generateZodSchema.test.ts | 21 ++++- src/core/generateZodSchema.ts | 131 +++++++++++++++++++---------- 2 files changed, 102 insertions(+), 50 deletions(-) diff --git a/src/core/generateZodSchema.test.ts b/src/core/generateZodSchema.test.ts index 099aa04c..5990bcd6 100644 --- a/src/core/generateZodSchema.test.ts +++ b/src/core/generateZodSchema.test.ts @@ -358,7 +358,7 @@ describe("generateZodSchema", () => { ); }); - it("should generate a schema with omit", () => { + it("should generate a schema with ", () => { const source = `export type InvincibleSuperman = Omit;`; expect(generate(source)).toMatchInlineSnapshot( `"export const invincibleSupermanSchema = supermanSchema.omit({ "weakness": true });"` @@ -376,9 +376,11 @@ describe("generateZodSchema", () => { const source = `export interface Superman extends Omit { withPower: boolean; }`; - expect(generate(source)).toMatchInlineSnapshot( - `"export const supermanSchema = clarkSchema.omit({ "weakness": true }).extend({ withPower: z.boolean() });"` - ); + expect(generate(source)).toMatchInlineSnapshot(` + "export const supermanSchema = clarkSchema.omit({ "weakness": true }).extend({ + withPower: z.boolean() + });" + `); }); it("should generate a schema with pick", () => { @@ -468,6 +470,17 @@ describe("generateZodSchema", () => { `); }); + it("should generate a schema with omit in interface extension clause and multiple clauses", () => { + const source = `export interface Superman extends KalL, Omit, Kryptonian { + withPower: boolean; + }`; + expect(generate(source)).toMatchInlineSnapshot(` + "export const supermanSchema = kalLSchema.extend(clarkSchema.omit({ "weakness": true }).shape).extend(kryptonianSchema.shape).extend({ + withPower: z.boolean() + });" + `); + }); + it("should deal with literal keys", () => { const source = `export interface Villain { "i.will.kill.everybody": true; diff --git a/src/core/generateZodSchema.ts b/src/core/generateZodSchema.ts index bcfaa3c4..37500e36 100644 --- a/src/core/generateZodSchema.ts +++ b/src/core/generateZodSchema.ts @@ -56,6 +56,12 @@ export interface GenerateZodSchemaProps { customJSDocFormatTypes: CustomJSDocFormatTypes; } +type SchemaExtensionClause = { + extendedSchemaName: string; + omitOrPickType?: "Omit" | "Pick"; + omitOrPickKeys?: ts.TypeNode; +}; + /** * Generate zod schema declaration * @@ -81,14 +87,14 @@ export function generateZodSchemaVariableStatement({ let requiresImport = false; if (ts.isInterfaceDeclaration(node)) { - let schemaExtensionClauses: string[] | undefined; + let schemaExtensionClauses: SchemaExtensionClause[] | undefined; if (node.typeParameters) { throw new Error("Interface with generics are not supported!"); } if (node.heritageClauses) { // Looping on heritageClauses browses the "extends" keywords schemaExtensionClauses = node.heritageClauses.reduce( - (deps: string[], h) => { + (deps: SchemaExtensionClause[], h) => { if (h.token !== ts.SyntaxKind.ExtendsKeyword || !h.types) { return deps; } @@ -101,11 +107,17 @@ export function generateZodSchemaVariableStatement({ ["Omit", "Pick"].includes(identifierName) && expression.typeArguments ) { - // do something here - // probably need to review schemaExtensionClauses to include Pick/Omit handling + const [originalType, keys] = expression.typeArguments; + return { + extendedSchemaName: getDependencyName( + originalType.getText(sourceFile) + ), + omitOrPickType: identifierName as "Omit" | "Pick", + omitOrPickKeys: keys, + }; } - return getDependencyName(identifierName); + return { extendedSchemaName: getDependencyName(identifierName) }; }); return deps.concat(heritages); @@ -113,7 +125,9 @@ export function generateZodSchemaVariableStatement({ [] ); - dependencies = dependencies.concat(schemaExtensionClauses); + dependencies = dependencies.concat( + schemaExtensionClauses.map((i) => i.extendedSchemaName) + ); } schema = buildZodObject({ @@ -525,16 +539,20 @@ function buildZodPrimitive({ // Deal with `Omit<>` & `Pick<>` syntax if (["Omit", "Pick"].includes(identifierName) && typeNode.typeArguments) { - return buildOmitPickObject( - identifierName, - typeNode.typeArguments, + const [originalType, keys] = typeNode.typeArguments; + const zodCall = buildZodPrimitive({ z, + typeNode: originalType, + isOptional: false, + jsDocTags: {}, sourceFile, dependencies, getDependencyName, skipParseJSDoc, - customJSDocFormatTypes - ); + customJSDocFormatTypes, + }); + + return buildOmitPickObject(identifierName, keys, sourceFile, zodCall); } const dependencyName = getDependencyName(identifierName); @@ -1001,23 +1019,58 @@ function buildZodSchema( } function buildZodExtendedSchema( - schemaList: string[], + schemaList: SchemaExtensionClause[], + sourceFile: ts.SourceFile, args?: ts.Expression[], properties?: ZodProperty[] ) { - let zodCall = f.createIdentifier(schemaList[0]) as ts.Expression; + let zodCall = f.createIdentifier( + schemaList[0].extendedSchemaName + ) as ts.Expression; + + if (schemaList[0].omitOrPickType && schemaList[0].omitOrPickKeys) { + const keys = schemaList[0].omitOrPickKeys; + const omitOrPickIdentifierName = schemaList[0].omitOrPickType; + zodCall = buildOmitPickObject( + omitOrPickIdentifierName, + keys, + sourceFile, + zodCall + ); + } for (let i = 1; i < schemaList.length; i++) { - zodCall = f.createCallExpression( - f.createPropertyAccessExpression(zodCall, f.createIdentifier("extend")), - undefined, - [ - f.createPropertyAccessExpression( - f.createIdentifier(schemaList[i]), - f.createIdentifier("shape") - ), - ] - ); + const omitOrPickIdentifierName = schemaList[i].omitOrPickType; + const keys = schemaList[i].omitOrPickKeys; + + if (omitOrPickIdentifierName && keys) { + zodCall = f.createCallExpression( + f.createPropertyAccessExpression(zodCall, f.createIdentifier("extend")), + undefined, + [ + f.createPropertyAccessExpression( + buildOmitPickObject( + omitOrPickIdentifierName, + keys, + sourceFile, + f.createIdentifier(schemaList[i].extendedSchemaName) + ), + f.createIdentifier("shape") + ), + ] + ); + } else { + zodCall = f.createCallExpression( + f.createPropertyAccessExpression(zodCall, f.createIdentifier("extend")), + undefined, + [ + f.createPropertyAccessExpression( + f.createIdentifier(schemaList[i].extendedSchemaName), + f.createIdentifier("shape") + ), + ] + ); + } } if (args?.length) { @@ -1073,7 +1126,7 @@ function buildZodObject({ dependencies: string[]; sourceFile: ts.SourceFile; getDependencyName: Required["getDependencyName"]; - schemaExtensionClauses?: string[]; + schemaExtensionClauses?: SchemaExtensionClause[]; skipParseJSDoc: boolean; customJSDocFormatTypes: CustomJSDocFormatTypes; }) { @@ -1117,6 +1170,7 @@ function buildZodObject({ if (schemaExtensionClauses && schemaExtensionClauses.length > 0) { objectSchema = buildZodExtendedSchema( schemaExtensionClauses, + sourceFile, properties.length > 0 ? [ f.createObjectLiteralExpression( @@ -1311,16 +1365,11 @@ function buildSchemaReference( } function buildOmitPickObject( - identifierName: string, - typeArguments: ts.NodeArray, - z: string, + omitOrPickIdentifierName: string, + keys: ts.TypeNode, sourceFile: ts.SourceFile, - dependencies: string[], - getDependencyName: (id: string) => string, - skipParseJSDoc: boolean, - customJSDocFormatTypes: CustomJSDocFormatTypes + zodCall: ts.Expression ) { - const [originalType, keys] = typeArguments; let parameters: ts.ObjectLiteralExpression | undefined; if (ts.isLiteralTypeNode(keys)) { @@ -1336,7 +1385,7 @@ function buildOmitPickObject( keys.types.map((type) => { if (!ts.isLiteralTypeNode(type)) { throw new Error( - `${identifierName} unknown syntax: (${ + `${omitOrPickIdentifierName} unknown syntax: (${ ts.SyntaxKind[type.kind] } as K union part not supported)` ); @@ -1351,7 +1400,7 @@ function buildOmitPickObject( if (!parameters) { throw new Error( - `${identifierName} unknown syntax: (${ + `${omitOrPickIdentifierName} unknown syntax: (${ ts.SyntaxKind[keys.kind] } as K not supported)` ); @@ -1359,18 +1408,8 @@ function buildOmitPickObject( return f.createCallExpression( f.createPropertyAccessExpression( - buildZodPrimitive({ - z, - typeNode: originalType, - isOptional: false, - jsDocTags: {}, - sourceFile, - dependencies, - getDependencyName, - skipParseJSDoc, - customJSDocFormatTypes, - }), - f.createIdentifier(lower(identifierName)) + zodCall, + f.createIdentifier(lower(omitOrPickIdentifierName)) ), undefined, [parameters] From 14b3756c82927b4f0b08f1aca3ad0789c2b4e5ac Mon Sep 17 00:00:00 2001 From: Thomas V <2619415+tvillaren@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:32:29 +0200 Subject: [PATCH 4/4] fix: typo --- src/core/generateZodSchema.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/generateZodSchema.test.ts b/src/core/generateZodSchema.test.ts index 5990bcd6..8bd46fbe 100644 --- a/src/core/generateZodSchema.test.ts +++ b/src/core/generateZodSchema.test.ts @@ -358,7 +358,7 @@ describe("generateZodSchema", () => { ); }); - it("should generate a schema with ", () => { + it("should generate a schema with omit ", () => { const source = `export type InvincibleSuperman = Omit;`; expect(generate(source)).toMatchInlineSnapshot( `"export const invincibleSupermanSchema = supermanSchema.omit({ "weakness": true });"`