From d54ba359eda7e74f7373e612153e8e408904b14a Mon Sep 17 00:00:00 2001 From: Morgan Intrator Date: Thu, 5 May 2022 22:23:54 -0400 Subject: [PATCH] feat: allow ctx.addIssue from transform (#1056) * feat: allow ctx.addIssue from transform * chore: update transform test * minor: lints --- .gitignore | 1 + deno/lib/__tests__/transformer.test.ts | 31 ++++++++++++++++++ deno/lib/types.ts | 44 ++++++++++++++------------ src/__tests__/transformer.test.ts | 31 ++++++++++++++++++ src/types.ts | 44 ++++++++++++++------------ 5 files changed, 109 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index cf410fbd6..1990b6b5e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ coverage src/playground.ts deno/lib/playground.ts .eslintcache +workspace.code-workspace diff --git a/deno/lib/__tests__/transformer.test.ts b/deno/lib/__tests__/transformer.test.ts index 2f69041df..fb1e48c34 100644 --- a/deno/lib/__tests__/transformer.test.ts +++ b/deno/lib/__tests__/transformer.test.ts @@ -11,6 +11,37 @@ const stringToNumber = z.string().transform((arg) => parseFloat(arg)); // .transform((n) => String(n)); const asyncNumberToString = z.number().transform(async (n) => String(n)); +test("transform ctx.addIssue", () => { + const strs = ["foo", "bar"]; + + expect(() => { + z.string() + .transform((data, ctx) => { + const i = strs.indexOf(data); + if (i === -1) { + ctx.addIssue({ + code: "custom", + message: `${data} is not one of our allowed strings`, + }); + } + return data.length; + }) + .parse("asdf"); + }).toThrow( + JSON.stringify( + [ + { + code: "custom", + message: "asdf is not one of our allowed strings", + path: [], + }, + ], + null, + 2 + ) + ); +}); + test("basic transformations", () => { const r1 = z .string() diff --git a/deno/lib/types.ts b/deno/lib/types.ts index a8a3754fd..0c3c160b8 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -401,7 +401,7 @@ export abstract class ZodType< } transform( - transform: (arg: Output) => NewOut | Promise + transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise ): ZodEffects { return new ZodEffects({ schema: this, @@ -3187,7 +3187,7 @@ export type RefinementEffect = { }; export type TransformEffect = { type: "transform"; - transform: (arg: T) => any; + transform: (arg: T, ctx: RefinementCtx) => any; }; export type PreprocessEffect = { type: "preprocess"; @@ -3239,23 +3239,22 @@ export class ZodEffects< } } - if (effect.type === "refinement") { - const checkCtx: RefinementCtx = { - addIssue: (arg: IssueData) => { - addIssueToContext(ctx, arg); - if (arg.fatal) { - status.abort(); - } else { - status.dirty(); - } - }, - get path() { - return ctx.path; - }, - }; - - checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + const checkCtx: RefinementCtx = { + addIssue: (arg: IssueData) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "refinement") { const executeRefinement = ( acc: unknown // effect: RefinementEffect @@ -3311,13 +3310,14 @@ export class ZodEffects< // } if (!isValid(base)) return base; - const result = effect.transform(base.value); + const result = effect.transform(base.value, checkCtx); if (result instanceof Promise) { throw new Error( `Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.` ); } - return OK(result); + + return { status: status.value, value: result }; } else { return this._def.schema ._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }) @@ -3327,7 +3327,9 @@ export class ZodEffects< // if (base.status === "dirty") { // return { status: "dirty", value: base.value }; // } - return Promise.resolve(effect.transform(base.value)).then(OK); + return Promise.resolve(effect.transform(base.value, checkCtx)).then( + OK + ); }); } } diff --git a/src/__tests__/transformer.test.ts b/src/__tests__/transformer.test.ts index 55e34f1d1..725802365 100644 --- a/src/__tests__/transformer.test.ts +++ b/src/__tests__/transformer.test.ts @@ -10,6 +10,37 @@ const stringToNumber = z.string().transform((arg) => parseFloat(arg)); // .transform((n) => String(n)); const asyncNumberToString = z.number().transform(async (n) => String(n)); +test("transform ctx.addIssue", () => { + const strs = ["foo", "bar"]; + + expect(() => { + z.string() + .transform((data, ctx) => { + const i = strs.indexOf(data); + if (i === -1) { + ctx.addIssue({ + code: "custom", + message: `${data} is not one of our allowed strings`, + }); + } + return data.length; + }) + .parse("asdf"); + }).toThrow( + JSON.stringify( + [ + { + code: "custom", + message: "asdf is not one of our allowed strings", + path: [], + }, + ], + null, + 2 + ) + ); +}); + test("basic transformations", () => { const r1 = z .string() diff --git a/src/types.ts b/src/types.ts index 877cda9d3..8d4018e75 100644 --- a/src/types.ts +++ b/src/types.ts @@ -401,7 +401,7 @@ export abstract class ZodType< } transform( - transform: (arg: Output) => NewOut | Promise + transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise ): ZodEffects { return new ZodEffects({ schema: this, @@ -3187,7 +3187,7 @@ export type RefinementEffect = { }; export type TransformEffect = { type: "transform"; - transform: (arg: T) => any; + transform: (arg: T, ctx: RefinementCtx) => any; }; export type PreprocessEffect = { type: "preprocess"; @@ -3239,23 +3239,22 @@ export class ZodEffects< } } - if (effect.type === "refinement") { - const checkCtx: RefinementCtx = { - addIssue: (arg: IssueData) => { - addIssueToContext(ctx, arg); - if (arg.fatal) { - status.abort(); - } else { - status.dirty(); - } - }, - get path() { - return ctx.path; - }, - }; - - checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + const checkCtx: RefinementCtx = { + addIssue: (arg: IssueData) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } else { + status.dirty(); + } + }, + get path() { + return ctx.path; + }, + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "refinement") { const executeRefinement = ( acc: unknown // effect: RefinementEffect @@ -3311,13 +3310,14 @@ export class ZodEffects< // } if (!isValid(base)) return base; - const result = effect.transform(base.value); + const result = effect.transform(base.value, checkCtx); if (result instanceof Promise) { throw new Error( `Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.` ); } - return OK(result); + + return { status: status.value, value: result }; } else { return this._def.schema ._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }) @@ -3327,7 +3327,9 @@ export class ZodEffects< // if (base.status === "dirty") { // return { status: "dirty", value: base.value }; // } - return Promise.resolve(effect.transform(base.value)).then(OK); + return Promise.resolve(effect.transform(base.value, checkCtx)).then( + OK + ); }); } }