diff --git a/README.md b/README.md index b99d67067..439cfecd2 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ - [Datetime](#datetime-validation) - [IP](#ip-address-validation) - [Numbers](#numbers) +- [BigInts](#bigints) - [NaNs](#nans) - [Booleans](#booleans) - [Dates](#dates) @@ -726,6 +727,24 @@ Optionally, you can pass in a second argument to provide a custom error message. z.number().lte(5, { message: "this👏is👏too👏big" }); ``` +## BigInts + +Zod includes a handful of bigint-specific validations. + +```ts +z.bigint().gt(5n); +z.bigint().gte(5n); // alias `.min(5n)` +z.bigint().lt(5n); +z.bigint().lte(5n); // alias `.max(5n)` + +z.bigint().positive(); // > 0n +z.bigint().nonnegative(); // >= 0n +z.bigint().negative(); // < 0n +z.bigint().nonpositive(); // <= 0n + +z.bigint().multipleOf(5n); // Evenly divisible by 5n. +``` + ## NaNs You can customize certain error messages when creating a nan schema. diff --git a/deno/lib/README.md b/deno/lib/README.md index b99d67067..439cfecd2 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -58,6 +58,7 @@ - [Datetime](#datetime-validation) - [IP](#ip-address-validation) - [Numbers](#numbers) +- [BigInts](#bigints) - [NaNs](#nans) - [Booleans](#booleans) - [Dates](#dates) @@ -726,6 +727,24 @@ Optionally, you can pass in a second argument to provide a custom error message. z.number().lte(5, { message: "this👏is👏too👏big" }); ``` +## BigInts + +Zod includes a handful of bigint-specific validations. + +```ts +z.bigint().gt(5n); +z.bigint().gte(5n); // alias `.min(5n)` +z.bigint().lt(5n); +z.bigint().lte(5n); // alias `.max(5n)` + +z.bigint().positive(); // > 0n +z.bigint().nonnegative(); // >= 0n +z.bigint().negative(); // < 0n +z.bigint().nonpositive(); // <= 0n + +z.bigint().multipleOf(5n); // Evenly divisible by 5n. +``` + ## NaNs You can customize certain error messages when creating a nan schema. diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index dcf4cc18a..def27ddfc 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -108,18 +108,18 @@ export interface ZodInvalidStringIssue extends ZodIssueBase { export interface ZodTooSmallIssue extends ZodIssueBase { code: typeof ZodIssueCode.too_small; - minimum: number; + minimum: number | bigint; inclusive: boolean; exact?: boolean; - type: "array" | "string" | "number" | "set" | "date"; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; } export interface ZodTooBigIssue extends ZodIssueBase { code: typeof ZodIssueCode.too_big; - maximum: number; + maximum: number | bigint; inclusive: boolean; exact?: boolean; - type: "array" | "string" | "number" | "set" | "date"; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; } export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { @@ -128,7 +128,7 @@ export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { export interface ZodNotMultipleOfIssue extends ZodIssueBase { code: typeof ZodIssueCode.not_multiple_of; - multipleOf: number; + multipleOf: number | bigint; } export interface ZodNotFiniteIssue extends ZodIssueBase { diff --git a/deno/lib/__tests__/bigint.test.ts b/deno/lib/__tests__/bigint.test.ts new file mode 100644 index 000000000..549a64819 --- /dev/null +++ b/deno/lib/__tests__/bigint.test.ts @@ -0,0 +1,58 @@ +// @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"; + +const gtFive = z.bigint().gt(BigInt(5)); +const gteFive = z.bigint().gte(BigInt(5)); +const ltFive = z.bigint().lt(BigInt(5)); +const lteFive = z.bigint().lte(BigInt(5)); +const positive = z.bigint().positive(); +const negative = z.bigint().negative(); +const nonnegative = z.bigint().nonnegative(); +const nonpositive = z.bigint().nonpositive(); +const multipleOfFive = z.bigint().multipleOf(BigInt(5)); + +test("passing validations", () => { + z.bigint().parse(BigInt(1)); + z.bigint().parse(BigInt(0)); + z.bigint().parse(BigInt(-1)); + gtFive.parse(BigInt(6)); + gteFive.parse(BigInt(5)); + gteFive.parse(BigInt(6)); + ltFive.parse(BigInt(4)); + lteFive.parse(BigInt(5)); + lteFive.parse(BigInt(4)); + positive.parse(BigInt(3)); + negative.parse(BigInt(-2)); + nonnegative.parse(BigInt(0)); + nonnegative.parse(BigInt(7)); + nonpositive.parse(BigInt(0)); + nonpositive.parse(BigInt(-12)); + multipleOfFive.parse(BigInt(15)); +}); + +test("failing validations", () => { + expect(() => gtFive.parse(BigInt(5))).toThrow(); + expect(() => gteFive.parse(BigInt(4))).toThrow(); + expect(() => ltFive.parse(BigInt(5))).toThrow(); + expect(() => lteFive.parse(BigInt(6))).toThrow(); + expect(() => positive.parse(BigInt(0))).toThrow(); + expect(() => positive.parse(BigInt(-2))).toThrow(); + expect(() => negative.parse(BigInt(0))).toThrow(); + expect(() => negative.parse(BigInt(3))).toThrow(); + expect(() => nonnegative.parse(BigInt(-1))).toThrow(); + expect(() => nonpositive.parse(BigInt(1))).toThrow(); + expect(() => multipleOfFive.parse(BigInt(13))).toThrow(); +}); + +test("min max getters", () => { + expect(z.bigint().min(BigInt(5)).minValue).toEqual(BigInt(5)); + expect(z.bigint().min(BigInt(5)).min(BigInt(10)).minValue).toEqual( + BigInt(10) + ); + + expect(z.bigint().max(BigInt(5)).maxValue).toEqual(BigInt(5)); + expect(z.bigint().max(BigInt(5)).max(BigInt(1)).maxValue).toEqual(BigInt(1)); +}); diff --git a/deno/lib/locales/en.ts b/deno/lib/locales/en.ts index 348a84eeb..30e564ce0 100644 --- a/deno/lib/locales/en.ts +++ b/deno/lib/locales/en.ts @@ -84,7 +84,7 @@ const errorMap: ZodErrorMap = (issue, _ctx) => { : issue.inclusive ? `greater than or equal to ` : `greater than ` - }${new Date(issue.minimum)}`; + }${new Date(Number(issue.minimum))}`; else message = "Invalid input"; break; case ZodIssueCode.too_big: @@ -104,6 +104,14 @@ const errorMap: ZodErrorMap = (issue, _ctx) => { ? `less than or equal to` : `less than` } ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${ + issue.exact + ? `exactly` + : issue.inclusive + ? `less than or equal to` + : `less than` + } ${issue.maximum}`; else if (issue.type === "date") message = `Date must be ${ issue.exact @@ -111,7 +119,7 @@ const errorMap: ZodErrorMap = (issue, _ctx) => { : issue.inclusive ? `smaller than or equal to` : `smaller than` - } ${new Date(issue.maximum)}`; + } ${new Date(Number(issue.maximum))}`; else message = "Invalid input"; break; case ZodIssueCode.custom: diff --git a/deno/lib/types.ts b/deno/lib/types.ts index ef1b70c15..da20a76c0 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -1276,8 +1276,13 @@ export class ZodNumber extends ZodType { ////////// ////////// ///////////////////////////////////////// ///////////////////////////////////////// +export type ZodBigIntCheck = + | { kind: "min"; value: bigint; inclusive: boolean; message?: string } + | { kind: "max"; value: bigint; inclusive: boolean; message?: string } + | { kind: "multipleOf"; value: bigint; message?: string }; export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; typeName: ZodFirstPartyTypeKind.ZodBigInt; coerce: boolean; } @@ -1297,18 +1302,178 @@ export class ZodBigInt extends ZodType { }); return INVALID; } - return OK(input.data); + + let ctx: undefined | ParseContext = undefined; + const status = new ParseStatus(); + + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive + ? input.data < check.value + : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } else if (check.kind === "max") { + const tooBig = check.inclusive + ? input.data > check.value + : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } else { + util.assertNever(check); + } + } + + return { status: status.value, value: input.data }; } static create = ( params?: RawCreateParams & { coerce?: boolean } ): ZodBigInt => { return new ZodBigInt({ + checks: [], typeName: ZodFirstPartyTypeKind.ZodBigInt, coerce: params?.coerce ?? false, ...processCreateParams(params), }); }; + + gte(value: bigint, message?: errorUtil.ErrMessage) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + min = this.gte; + + gt(value: bigint, message?: errorUtil.ErrMessage) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + + lte(value: bigint, message?: errorUtil.ErrMessage) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + max = this.lte; + + lt(value: bigint, message?: errorUtil.ErrMessage) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + + protected setLimit( + kind: "min" | "max", + value: bigint, + inclusive: boolean, + message?: string + ) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + + _addCheck(check: ZodBigIntCheck) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + + positive(message?: errorUtil.ErrMessage) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + + negative(message?: errorUtil.ErrMessage) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + + nonpositive(message?: errorUtil.ErrMessage) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + + nonnegative(message?: errorUtil.ErrMessage) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + + multipleOf(value: bigint, message?: errorUtil.ErrMessage) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + + get minValue() { + let min: bigint | null = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) min = ch.value; + } + } + return min; + } + + get maxValue() { + let max: bigint | null = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) max = ch.value; + } + } + return max; + } } ////////////////////////////////////////// diff --git a/src/ZodError.ts b/src/ZodError.ts index 0833ae614..0802c1793 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -108,18 +108,18 @@ export interface ZodInvalidStringIssue extends ZodIssueBase { export interface ZodTooSmallIssue extends ZodIssueBase { code: typeof ZodIssueCode.too_small; - minimum: number; + minimum: number | bigint; inclusive: boolean; exact?: boolean; - type: "array" | "string" | "number" | "set" | "date"; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; } export interface ZodTooBigIssue extends ZodIssueBase { code: typeof ZodIssueCode.too_big; - maximum: number; + maximum: number | bigint; inclusive: boolean; exact?: boolean; - type: "array" | "string" | "number" | "set" | "date"; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; } export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { @@ -128,7 +128,7 @@ export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { export interface ZodNotMultipleOfIssue extends ZodIssueBase { code: typeof ZodIssueCode.not_multiple_of; - multipleOf: number; + multipleOf: number | bigint; } export interface ZodNotFiniteIssue extends ZodIssueBase { diff --git a/src/__tests__/bigint.test.ts b/src/__tests__/bigint.test.ts new file mode 100644 index 000000000..1aea6fcdb --- /dev/null +++ b/src/__tests__/bigint.test.ts @@ -0,0 +1,57 @@ +// @ts-ignore TS6133 +import { expect, test } from "@jest/globals"; + +import * as z from "../index"; + +const gtFive = z.bigint().gt(BigInt(5)); +const gteFive = z.bigint().gte(BigInt(5)); +const ltFive = z.bigint().lt(BigInt(5)); +const lteFive = z.bigint().lte(BigInt(5)); +const positive = z.bigint().positive(); +const negative = z.bigint().negative(); +const nonnegative = z.bigint().nonnegative(); +const nonpositive = z.bigint().nonpositive(); +const multipleOfFive = z.bigint().multipleOf(BigInt(5)); + +test("passing validations", () => { + z.bigint().parse(BigInt(1)); + z.bigint().parse(BigInt(0)); + z.bigint().parse(BigInt(-1)); + gtFive.parse(BigInt(6)); + gteFive.parse(BigInt(5)); + gteFive.parse(BigInt(6)); + ltFive.parse(BigInt(4)); + lteFive.parse(BigInt(5)); + lteFive.parse(BigInt(4)); + positive.parse(BigInt(3)); + negative.parse(BigInt(-2)); + nonnegative.parse(BigInt(0)); + nonnegative.parse(BigInt(7)); + nonpositive.parse(BigInt(0)); + nonpositive.parse(BigInt(-12)); + multipleOfFive.parse(BigInt(15)); +}); + +test("failing validations", () => { + expect(() => gtFive.parse(BigInt(5))).toThrow(); + expect(() => gteFive.parse(BigInt(4))).toThrow(); + expect(() => ltFive.parse(BigInt(5))).toThrow(); + expect(() => lteFive.parse(BigInt(6))).toThrow(); + expect(() => positive.parse(BigInt(0))).toThrow(); + expect(() => positive.parse(BigInt(-2))).toThrow(); + expect(() => negative.parse(BigInt(0))).toThrow(); + expect(() => negative.parse(BigInt(3))).toThrow(); + expect(() => nonnegative.parse(BigInt(-1))).toThrow(); + expect(() => nonpositive.parse(BigInt(1))).toThrow(); + expect(() => multipleOfFive.parse(BigInt(13))).toThrow(); +}); + +test("min max getters", () => { + expect(z.bigint().min(BigInt(5)).minValue).toEqual(BigInt(5)); + expect(z.bigint().min(BigInt(5)).min(BigInt(10)).minValue).toEqual( + BigInt(10) + ); + + expect(z.bigint().max(BigInt(5)).maxValue).toEqual(BigInt(5)); + expect(z.bigint().max(BigInt(5)).max(BigInt(1)).maxValue).toEqual(BigInt(1)); +}); diff --git a/src/locales/en.ts b/src/locales/en.ts index 6515f9849..f09f48735 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -84,7 +84,7 @@ const errorMap: ZodErrorMap = (issue, _ctx) => { : issue.inclusive ? `greater than or equal to ` : `greater than ` - }${new Date(issue.minimum)}`; + }${new Date(Number(issue.minimum))}`; else message = "Invalid input"; break; case ZodIssueCode.too_big: @@ -104,6 +104,14 @@ const errorMap: ZodErrorMap = (issue, _ctx) => { ? `less than or equal to` : `less than` } ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${ + issue.exact + ? `exactly` + : issue.inclusive + ? `less than or equal to` + : `less than` + } ${issue.maximum}`; else if (issue.type === "date") message = `Date must be ${ issue.exact @@ -111,7 +119,7 @@ const errorMap: ZodErrorMap = (issue, _ctx) => { : issue.inclusive ? `smaller than or equal to` : `smaller than` - } ${new Date(issue.maximum)}`; + } ${new Date(Number(issue.maximum))}`; else message = "Invalid input"; break; case ZodIssueCode.custom: diff --git a/src/types.ts b/src/types.ts index 03c52f002..cd01a65e3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1276,8 +1276,13 @@ export class ZodNumber extends ZodType { ////////// ////////// ///////////////////////////////////////// ///////////////////////////////////////// +export type ZodBigIntCheck = + | { kind: "min"; value: bigint; inclusive: boolean; message?: string } + | { kind: "max"; value: bigint; inclusive: boolean; message?: string } + | { kind: "multipleOf"; value: bigint; message?: string }; export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; typeName: ZodFirstPartyTypeKind.ZodBigInt; coerce: boolean; } @@ -1297,18 +1302,178 @@ export class ZodBigInt extends ZodType { }); return INVALID; } - return OK(input.data); + + let ctx: undefined | ParseContext = undefined; + const status = new ParseStatus(); + + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive + ? input.data < check.value + : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } else if (check.kind === "max") { + const tooBig = check.inclusive + ? input.data > check.value + : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message, + }); + status.dirty(); + } + } else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message, + }); + status.dirty(); + } + } else { + util.assertNever(check); + } + } + + return { status: status.value, value: input.data }; } static create = ( params?: RawCreateParams & { coerce?: boolean } ): ZodBigInt => { return new ZodBigInt({ + checks: [], typeName: ZodFirstPartyTypeKind.ZodBigInt, coerce: params?.coerce ?? false, ...processCreateParams(params), }); }; + + gte(value: bigint, message?: errorUtil.ErrMessage) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + min = this.gte; + + gt(value: bigint, message?: errorUtil.ErrMessage) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + + lte(value: bigint, message?: errorUtil.ErrMessage) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + max = this.lte; + + lt(value: bigint, message?: errorUtil.ErrMessage) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + + protected setLimit( + kind: "min" | "max", + value: bigint, + inclusive: boolean, + message?: string + ) { + return new ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message), + }, + ], + }); + } + + _addCheck(check: ZodBigIntCheck) { + return new ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check], + }); + } + + positive(message?: errorUtil.ErrMessage) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + + negative(message?: errorUtil.ErrMessage) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message), + }); + } + + nonpositive(message?: errorUtil.ErrMessage) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + + nonnegative(message?: errorUtil.ErrMessage) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message), + }); + } + + multipleOf(value: bigint, message?: errorUtil.ErrMessage) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message), + }); + } + + get minValue() { + let min: bigint | null = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) min = ch.value; + } + } + return min; + } + + get maxValue() { + let max: bigint | null = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) max = ch.value; + } + } + return max; + } } //////////////////////////////////////////