Add ZodPipeline

This commit is contained in:
Colin McDonnell 2022-11-13 23:44:42 -08:00
parent 0ce88be33d
commit bcef014180
10 changed files with 1004 additions and 1015 deletions

2
.gitignore vendored
View File

@ -5,7 +5,7 @@ coverage
.vscode
.idea
*.log
src/playground.ts
playground.ts
deno/lib/playground.ts
.eslintcache
workspace.code-workspace

View File

@ -75,6 +75,8 @@ test("first party switch", () => {
break;
case z.ZodFirstPartyTypeKind.ZodBranded:
break;
case z.ZodFirstPartyTypeKind.ZodPipeline:
break;
default:
util.assertNever(def);
}

View File

@ -0,0 +1,30 @@
// @ts-ignore TS6133
import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts";
const test = Deno.test;
import * as z from "../index.ts";
test("string to number pipeline", () => {
const schema = z.string().transform(Number).pipe(z.number());
expect(schema.parse("1234")).toEqual(1234);
});
test("string to number pipeline async", async () => {
const schema = z
.string()
.transform(async (val) => Number(val))
.pipe(z.number());
expect(await schema.parseAsync("1234")).toEqual(1234);
});
test("break if dirty", () => {
const schema = z
.string()
.refine((c) => c === "1234")
.transform(async (val) => Number(val))
.pipe(z.number().refine((v) => v < 100));
const r1: any = schema.safeParse("12345");
expect(r1.error.issues.length).toBe(1);
const r2: any = schema.safeParse("3");
expect(r2.error.issues.length).toBe(1);
});

View File

@ -371,6 +371,7 @@ export abstract class ZodType<
this.default = this.default.bind(this);
this.catch = this.catch.bind(this);
this.describe = this.describe.bind(this);
this.pipe = this.pipe.bind(this);
this.isNullable = this.isNullable.bind(this);
this.isOptional = this.isOptional.bind(this);
}
@ -448,6 +449,10 @@ export abstract class ZodType<
});
}
pipe<T extends ZodTypeAny>(target: T): ZodPipeline<this, T> {
return ZodPipeline.create(this, target);
}
isOptional(): boolean {
return this.safeParse(undefined).success;
}
@ -1279,7 +1284,7 @@ export interface ZodAnyDef extends ZodTypeDef {
export class ZodAny extends ZodType<any, ZodAnyDef> {
// to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject.
_any: true = true;
_any = true as const;
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
return OK(input.data);
}
@ -1304,7 +1309,7 @@ export interface ZodUnknownDef extends ZodTypeDef {
export class ZodUnknown extends ZodType<unknown, ZodUnknownDef> {
// required
_unknown: true = true;
_unknown = true as const;
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
return OK(input.data);
}
@ -3914,6 +3919,82 @@ export class ZodBranded<
}
}
////////////////////////////////////////////
////////////////////////////////////////////
////////// //////////
////////// ZodPipeline //////////
////////// //////////
////////////////////////////////////////////
////////////////////////////////////////////
export interface ZodPipelineDef<A extends ZodTypeAny, B extends ZodTypeAny>
extends ZodTypeDef {
in: A;
out: B;
typeName: ZodFirstPartyTypeKind.ZodPipeline;
}
export class ZodPipeline<
A extends ZodTypeAny,
B extends ZodTypeAny
> extends ZodType<B["_output"], ZodPipelineDef<A, B>, A["_input"]> {
_parse(input: ParseInput): ParseReturnType<any> {
const { status, ctx } = this._processInputParams(input);
if (ctx.common.async) {
const handleAsync = async () => {
const inResult = await this._def.in._parseAsync({
data: ctx.data,
path: ctx.path,
parent: ctx,
});
if (inResult.status === "aborted") return INVALID;
if (inResult.status === "dirty") {
status.dirty();
return DIRTY(inResult.value);
} else {
return this._def.out._parseAsync({
data: inResult.value,
path: ctx.path,
parent: ctx,
});
}
};
return handleAsync();
} else {
const inResult = this._def.in._parseSync({
data: ctx.data,
path: ctx.path,
parent: ctx,
});
if (inResult.status === "aborted") return INVALID;
if (inResult.status === "dirty") {
status.dirty();
return {
status: "dirty",
value: inResult.value,
};
} else {
return this._def.out._parseSync({
data: inResult.value,
path: ctx.path,
parent: ctx,
});
}
}
}
static create<A extends ZodTypeAny, B extends ZodTypeAny>(
a: A,
b: B
): ZodPipeline<A, B> {
return new ZodPipeline({
in: a,
out: b,
typeName: ZodFirstPartyTypeKind.ZodPipeline,
});
}
}
export const custom = <T>(
check?: (data: unknown) => any,
params: Parameters<ZodTypeAny["refine"]>[1] = {},
@ -3970,6 +4051,7 @@ export enum ZodFirstPartyTypeKind {
ZodCatch = "ZodCatch",
ZodPromise = "ZodPromise",
ZodBranded = "ZodBranded",
ZodPipeline = "ZodPipeline",
}
export type ZodFirstPartySchemaTypes =
| ZodString
@ -4004,7 +4086,8 @@ export type ZodFirstPartySchemaTypes =
| ZodDefault<any>
| ZodCatch<any>
| ZodPromise<any>
| ZodBranded<any, any>;
| ZodBranded<any, any>
| ZodPipeline<any, any>;
// new approach that works for abstract classes
// but required TS 4.4+
@ -4051,6 +4134,7 @@ const effectsType = ZodEffects.create;
const optionalType = ZodOptional.create;
const nullableType = ZodNullable.create;
const preprocessType = ZodEffects.createWithPreprocess;
const pipelineType = ZodPipeline.create;
const ostring = () => stringType().optional();
const onumber = () => numberType().optional();
const oboolean = () => booleanType().optional();
@ -4081,6 +4165,7 @@ export {
onumber,
optionalType as optional,
ostring,
pipelineType as pipeline,
preprocessType as preprocess,
promiseType as promise,
recordType as record,

View File

@ -4,22 +4,6 @@
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "src/.*\\.test\\.ts$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
"coverageReporters": [
"json-summary",
"text",
"lcov"
],
"globals": {
"ts-jest": {
"tsconfig": "tsconfig.json"
}
}
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
"coverageReporters": ["json-summary", "text", "lcov"]
}

View File

@ -60,7 +60,7 @@
"test": "jest --coverage",
"test:deno": "cd deno && deno test",
"prepublishOnly": "npm run test && npm run build && npm run build:deno",
"play": "nodemon -e ts -w . -x tsx src/playground.ts",
"play": "nodemon -e ts -w . -x tsx playground.ts",
"depcruise": "depcruise -c .dependency-cruiser.js src",
"benchmark": "tsx src/benchmarks/index.ts",
"prepare": "husky install"
@ -68,7 +68,7 @@
"devDependencies": {
"@rollup/plugin-typescript": "^8.2.0",
"@types/benchmark": "^2.1.0",
"@types/jest": "^26.0.17",
"@types/jest": "^29.2.2",
"@types/node": "14",
"@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0",
@ -81,13 +81,13 @@
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"husky": "^7.0.4",
"jest": "^27.5.1",
"jest": "^29.3.1",
"lint-staged": "^12.3.7",
"nodemon": "^2.0.15",
"prettier": "^2.6.0",
"pretty-quick": "^3.1.3",
"rollup": "^2.70.1",
"ts-jest": "^27.1.3",
"ts-jest": "^29.0.3",
"ts-morph": "^14.0.0",
"ts-node": "^10.9.1",
"tslib": "^2.3.1",

View File

@ -74,6 +74,8 @@ test("first party switch", () => {
break;
case z.ZodFirstPartyTypeKind.ZodBranded:
break;
case z.ZodFirstPartyTypeKind.ZodPipeline:
break;
default:
util.assertNever(def);
}

View File

@ -0,0 +1,29 @@
// @ts-ignore TS6133
import { expect, test } from "@jest/globals";
import * as z from "../index";
test("string to number pipeline", () => {
const schema = z.string().transform(Number).pipe(z.number());
expect(schema.parse("1234")).toEqual(1234);
});
test("string to number pipeline async", async () => {
const schema = z
.string()
.transform(async (val) => Number(val))
.pipe(z.number());
expect(await schema.parseAsync("1234")).toEqual(1234);
});
test("break if dirty", () => {
const schema = z
.string()
.refine((c) => c === "1234")
.transform(async (val) => Number(val))
.pipe(z.number().refine((v) => v < 100));
const r1: any = schema.safeParse("12345");
expect(r1.error.issues.length).toBe(1);
const r2: any = schema.safeParse("3");
expect(r2.error.issues.length).toBe(1);
});

View File

@ -371,6 +371,7 @@ export abstract class ZodType<
this.default = this.default.bind(this);
this.catch = this.catch.bind(this);
this.describe = this.describe.bind(this);
this.pipe = this.pipe.bind(this);
this.isNullable = this.isNullable.bind(this);
this.isOptional = this.isOptional.bind(this);
}
@ -448,6 +449,10 @@ export abstract class ZodType<
});
}
pipe<T extends ZodTypeAny>(target: T): ZodPipeline<this, T> {
return ZodPipeline.create(this, target);
}
isOptional(): boolean {
return this.safeParse(undefined).success;
}
@ -1279,7 +1284,7 @@ export interface ZodAnyDef extends ZodTypeDef {
export class ZodAny extends ZodType<any, ZodAnyDef> {
// to prevent instances of other classes from extending ZodAny. this causes issues with catchall in ZodObject.
_any: true = true;
_any = true as const;
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
return OK(input.data);
}
@ -1304,7 +1309,7 @@ export interface ZodUnknownDef extends ZodTypeDef {
export class ZodUnknown extends ZodType<unknown, ZodUnknownDef> {
// required
_unknown: true = true;
_unknown = true as const;
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
return OK(input.data);
}
@ -3914,6 +3919,82 @@ export class ZodBranded<
}
}
////////////////////////////////////////////
////////////////////////////////////////////
////////// //////////
////////// ZodPipeline //////////
////////// //////////
////////////////////////////////////////////
////////////////////////////////////////////
export interface ZodPipelineDef<A extends ZodTypeAny, B extends ZodTypeAny>
extends ZodTypeDef {
in: A;
out: B;
typeName: ZodFirstPartyTypeKind.ZodPipeline;
}
export class ZodPipeline<
A extends ZodTypeAny,
B extends ZodTypeAny
> extends ZodType<B["_output"], ZodPipelineDef<A, B>, A["_input"]> {
_parse(input: ParseInput): ParseReturnType<any> {
const { status, ctx } = this._processInputParams(input);
if (ctx.common.async) {
const handleAsync = async () => {
const inResult = await this._def.in._parseAsync({
data: ctx.data,
path: ctx.path,
parent: ctx,
});
if (inResult.status === "aborted") return INVALID;
if (inResult.status === "dirty") {
status.dirty();
return DIRTY(inResult.value);
} else {
return this._def.out._parseAsync({
data: inResult.value,
path: ctx.path,
parent: ctx,
});
}
};
return handleAsync();
} else {
const inResult = this._def.in._parseSync({
data: ctx.data,
path: ctx.path,
parent: ctx,
});
if (inResult.status === "aborted") return INVALID;
if (inResult.status === "dirty") {
status.dirty();
return {
status: "dirty",
value: inResult.value,
};
} else {
return this._def.out._parseSync({
data: inResult.value,
path: ctx.path,
parent: ctx,
});
}
}
}
static create<A extends ZodTypeAny, B extends ZodTypeAny>(
a: A,
b: B
): ZodPipeline<A, B> {
return new ZodPipeline({
in: a,
out: b,
typeName: ZodFirstPartyTypeKind.ZodPipeline,
});
}
}
export const custom = <T>(
check?: (data: unknown) => any,
params: Parameters<ZodTypeAny["refine"]>[1] = {},
@ -3970,6 +4051,7 @@ export enum ZodFirstPartyTypeKind {
ZodCatch = "ZodCatch",
ZodPromise = "ZodPromise",
ZodBranded = "ZodBranded",
ZodPipeline = "ZodPipeline",
}
export type ZodFirstPartySchemaTypes =
| ZodString
@ -4004,7 +4086,8 @@ export type ZodFirstPartySchemaTypes =
| ZodDefault<any>
| ZodCatch<any>
| ZodPromise<any>
| ZodBranded<any, any>;
| ZodBranded<any, any>
| ZodPipeline<any, any>;
// new approach that works for abstract classes
// but required TS 4.4+
@ -4051,6 +4134,7 @@ const effectsType = ZodEffects.create;
const optionalType = ZodOptional.create;
const nullableType = ZodNullable.create;
const preprocessType = ZodEffects.createWithPreprocess;
const pipelineType = ZodPipeline.create;
const ostring = () => stringType().optional();
const onumber = () => numberType().optional();
const oboolean = () => booleanType().optional();
@ -4081,6 +4165,7 @@ export {
onumber,
optionalType as optional,
ostring,
pipelineType as pipeline,
preprocessType as preprocess,
promiseType as promise,
recordType as record,

1744
yarn.lock

File diff suppressed because it is too large Load Diff