Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow ctx.addIssue from transform #1056

Merged
merged 3 commits into from May 6, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions deno/lib/__tests__/transformer.test.ts
Expand Up @@ -11,6 +11,29 @@ 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();
});

test("basic transformations", () => {
const r1 = z
.string()
Expand Down
44 changes: 23 additions & 21 deletions deno/lib/types.ts
Expand Up @@ -422,7 +422,7 @@ export abstract class ZodType<
}

transform<NewOut>(
transform: (arg: Output) => NewOut | Promise<NewOut>
transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise<NewOut>
): ZodEffects<this, NewOut> {
return new ZodEffects({
schema: this,
Expand Down Expand Up @@ -3219,7 +3219,7 @@ export type RefinementEffect<T> = {
};
export type TransformEffect<T> = {
type: "transform";
transform: (arg: T) => any;
transform: (arg: T, ctx: RefinementCtx) => any;
};
export type PreprocessEffect<T> = {
type: "preprocess";
Expand Down Expand Up @@ -3271,23 +3271,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<any>
Expand Down Expand Up @@ -3343,13 +3342,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 })
Expand All @@ -3359,7 +3359,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
);
});
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/__tests__/transformer.test.ts
Expand Up @@ -10,6 +10,29 @@ 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if this test could check for the kind of error, ideally the custom code/message.

Otherwise, this test would also pass if ctx is undefined as it would throw a JS error

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Checking the whole error message now.

});

test("basic transformations", () => {
const r1 = z
.string()
Expand Down
44 changes: 23 additions & 21 deletions src/types.ts
Expand Up @@ -422,7 +422,7 @@ export abstract class ZodType<
}

transform<NewOut>(
transform: (arg: Output) => NewOut | Promise<NewOut>
transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise<NewOut>
): ZodEffects<this, NewOut> {
return new ZodEffects({
schema: this,
Expand Down Expand Up @@ -3219,7 +3219,7 @@ export type RefinementEffect<T> = {
};
export type TransformEffect<T> = {
type: "transform";
transform: (arg: T) => any;
transform: (arg: T, ctx: RefinementCtx) => any;
};
export type PreprocessEffect<T> = {
type: "preprocess";
Expand Down Expand Up @@ -3271,23 +3271,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<any>
Expand Down Expand Up @@ -3343,13 +3342,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 })
Expand All @@ -3359,7 +3359,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
);
});
}
}
Expand Down