Skip to content

Commit

Permalink
Merge branch 'master' of github.com:vriad/zod
Browse files Browse the repository at this point in the history
  • Loading branch information
Morita0711 committed Sep 3, 2021
2 parents 5777880 + b09b1c6 commit 5f998be
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 4 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
- [.merge](#merge)
- [.pick/.omit](#pickomit)
- [.partial](#partial)
- [.partialBy](#partialBy)
- [.deepPartial](#deepPartial)
- [.passthrough](#passthrough)
- [.strict](#strict)
Expand Down Expand Up @@ -242,7 +243,7 @@ const User = z.object({
username: z.string(),
});

User.parse({ username: string });
User.parse({ username: "Ludwig" });

// extract the inferred type
type User = z.infer<typeof User>;
Expand Down Expand Up @@ -343,6 +344,8 @@ z.number().positive(); // > 0
z.number().nonnegative(); // >= 0
z.number().negative(); // < 0
z.number().nonpositive(); // <= 0

z.number().multipleOf(5); // Evenly divisible by 5. Alias .step(5)
```

Optionally, you can pass in a second argument to provide a custom error message.
Expand Down Expand Up @@ -454,6 +457,34 @@ const partialUser = user.partial();
// { username?: string | undefined }
```

### `.partialBy`

All Zod object schemas have a `.partialBy` method which returns a modified version with the specified properties optional.

Starting from this object:

```ts
const Recipe = z.object({
id: z.string(),
name: z.string(),
ingredients: z.array(z.string()),
});
```

To make certain keys optional:

```ts
const OptionalName = Recipe.partialBy({ name: true });
type OptionalName = z.infer<typeof OptionalName>;
/*
{
id: string,
name?: string | undefined,
ingredients: string[]
}
*/
```

### `.deepPartial`

The `.partial` method is shallow — it only applies one level deep. There is also a "deep" version:
Expand Down
2 changes: 2 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ z.number().positive(); // > 0
z.number().nonnegative(); // >= 0
z.number().negative(); // < 0
z.number().nonpositive(); // <= 0

z.number().multipleOf(5); // x % 5 === 0
```

你可以选择传入第二个参数来提供一个自定义的错误信息。
Expand Down
2 changes: 1 addition & 1 deletion coverage.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions deno/lib/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const ZodIssueCode = util.arrayToEnum([
"too_small",
"too_big",
"invalid_intersection_types",
"not_multiple_of",
]);

export type ZodIssueCode = keyof typeof ZodIssueCode;
Expand Down Expand Up @@ -84,6 +85,11 @@ export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_intersection_types;
}

export interface ZodNotMultipleOfIssue extends ZodIssueBase {
code: typeof ZodIssueCode.not_multiple_of;
multipleOf: number;
}

export interface ZodCustomIssue extends ZodIssueBase {
code: typeof ZodIssueCode.custom;
params?: { [k: string]: any };
Expand All @@ -103,6 +109,7 @@ export type ZodIssueOptionalMessage =
| ZodTooSmallIssue
| ZodTooBigIssue
| ZodInvalidIntersectionTypesIssue
| ZodNotMultipleOfIssue
| ZodCustomIssue;

export type ZodIssue = ZodIssueOptionalMessage & { message: string };
Expand Down Expand Up @@ -326,6 +333,9 @@ export const defaultErrorMap = (
case ZodIssueCode.invalid_intersection_types:
message = `Intersection results could not be merged`;
break;
case ZodIssueCode.not_multiple_of:
message = `Should be multiple of ${error.multipleOf}`;
break;
default:
message = _ctx.defaultError;
util.assertNever(error);
Expand Down
6 changes: 6 additions & 0 deletions deno/lib/__tests__/number.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ const gteFive = z.number().gte(5);
const ltFive = z.number().lt(5);
const lteFive = z.number().lte(5);
const intNum = z.number().int();
const multipleOfFive = z.number().multipleOf(5);
const stepSixPointFour = z.number().step(6.4);

test("passing validations", () => {
gtFive.parse(6);
gteFive.parse(5);
ltFive.parse(4);
lteFive.parse(5);
intNum.parse(4);
multipleOfFive.parse(15);
stepSixPointFour.parse(12.8);
});

test("failing validations", () => {
Expand All @@ -24,6 +28,8 @@ test("failing validations", () => {
expect(() => gtFive.parse(5)).toThrow();
expect(() => gteFive.parse(4)).toThrow();
expect(() => intNum.parse(3.14)).toThrow();
expect(() => multipleOfFive.parse(14.9)).toThrow();
expect(() => stepSixPointFour.parse(6.41)).toThrow();
});

test("parse NaN", () => {
Expand Down
43 changes: 43 additions & 0 deletions deno/lib/__tests__/partialBy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @ts-ignore TS6133
import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts";
const test = Deno.test;

import { util } from "../helpers/util.ts";
import * as z from "../index.ts";

const fish = z.object({
name: z.string(),
age: z.number(),
nested: z.object({}),
});

test("partial by type inference", () => {
const optionalNameFish = fish.partialBy({ name: true });
type optionalNameFish = z.infer<typeof optionalNameFish>;
const f1: util.AssertEqual<
optionalNameFish,
{ name?: string | undefined; age: number; nested: {} }
> = true;
f1;
});

test("partial by parse - success", () => {
const nameOptionalFish = fish.partialBy({ name: true });
nameOptionalFish.parse({ age: 0, nested: {} });
});

test("partial by parse - fail", () => {
fish.partialBy({ name: true, age: true }).parse({ nested: {} } as any);
fish
.partialBy({ name: true })
.parse({ name: "bob", age: 12, nested: {} } as any);
fish.partialBy({ name: true }).parse({ age: 12, nested: {} } as any);

const nameOptionalFish = fish.partialBy({ name: true });
const bad1 = () =>
nameOptionalFish.parse({ name: 12, age: 12, nested: {} } as any);
const bad2 = () => nameOptionalFish.parse({ age: 12 } as any);

expect(bad1).toThrow();
expect(bad2).toThrow();
});
53 changes: 52 additions & 1 deletion deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,8 @@ export class ZodString extends ZodType<string, ZodStringDef> {
type ZodNumberCheck =
| { kind: "min"; value: number; inclusive: boolean; message?: string }
| { kind: "max"; value: number; inclusive: boolean; message?: string }
| { kind: "int"; message?: string };
| { kind: "int"; message?: string }
| { kind: "multipleOf"; value: number; message?: string };

export interface ZodNumberDef extends ZodTypeDef {
checks: ZodNumberCheck[];
Expand Down Expand Up @@ -622,6 +623,15 @@ export class ZodNumber extends ZodType<number, ZodNumberDef> {
message: check.message,
});
}
} else if (check.kind === "multipleOf") {
if (data % check.value !== 0) {
invalid = true;
ctx.addIssue(data, {
code: ZodIssueCode.not_multiple_of,
multipleOf: check.value,
message: check.message,
});
}
}
}

Expand Down Expand Up @@ -736,6 +746,20 @@ export class ZodNumber extends ZodType<number, ZodNumberDef> {
],
});

multipleOf = (value: number, message?: errorUtil.ErrMessage) =>
new ZodNumber({
...this._def,
checks: [
...this._def.checks,
{
kind: "multipleOf",
value: value,
message: errorUtil.toString(message),
},
],
});
step = this.multipleOf;

get minValue() {
let min: number | null = null;
for (const ch of this._def.checks) {
Expand Down Expand Up @@ -1668,6 +1692,33 @@ export class ZodObject<
return deepPartialify(this) as any;
};

partialBy = <Mask extends { [k in keyof T]?: true }>(
mask: Mask
): ZodObject<
objectUtil.noNever<
{
[k in keyof T]: k extends keyof Mask
? ReturnType<T[k]["optional"]>
: T[k];
}
>,
UnknownKeys,
Catchall
> => {
const newShape: any = {};
util.objectKeys(this.shape).map((key) => {
if (util.objectKeys(mask).indexOf(key) === -1) {
newShape[key] = this.shape[key];
} else {
newShape[key] = this.shape[key].optional();
}
});
return new ZodObject({
...this._def,
shape: () => newShape,
}) as any;
};

required = (): ZodObject<
{ [k in keyof T]: deoptional<T[k]> },
UnknownKeys,
Expand Down
10 changes: 10 additions & 0 deletions src/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const ZodIssueCode = util.arrayToEnum([
"too_small",
"too_big",
"invalid_intersection_types",
"not_multiple_of",
]);

export type ZodIssueCode = keyof typeof ZodIssueCode;
Expand Down Expand Up @@ -84,6 +85,11 @@ export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_intersection_types;
}

export interface ZodNotMultipleOfIssue extends ZodIssueBase {
code: typeof ZodIssueCode.not_multiple_of;
multipleOf: number;
}

export interface ZodCustomIssue extends ZodIssueBase {
code: typeof ZodIssueCode.custom;
params?: { [k: string]: any };
Expand All @@ -103,6 +109,7 @@ export type ZodIssueOptionalMessage =
| ZodTooSmallIssue
| ZodTooBigIssue
| ZodInvalidIntersectionTypesIssue
| ZodNotMultipleOfIssue
| ZodCustomIssue;

export type ZodIssue = ZodIssueOptionalMessage & { message: string };
Expand Down Expand Up @@ -326,6 +333,9 @@ export const defaultErrorMap = (
case ZodIssueCode.invalid_intersection_types:
message = `Intersection results could not be merged`;
break;
case ZodIssueCode.not_multiple_of:
message = `Should be multiple of ${error.multipleOf}`;
break;
default:
message = _ctx.defaultError;
util.assertNever(error);
Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/number.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ const gteFive = z.number().gte(5);
const ltFive = z.number().lt(5);
const lteFive = z.number().lte(5);
const intNum = z.number().int();
const multipleOfFive = z.number().multipleOf(5);
const stepSixPointFour = z.number().step(6.4);

test("passing validations", () => {
gtFive.parse(6);
gteFive.parse(5);
ltFive.parse(4);
lteFive.parse(5);
intNum.parse(4);
multipleOfFive.parse(15);
stepSixPointFour.parse(12.8);
});

test("failing validations", () => {
Expand All @@ -23,6 +27,8 @@ test("failing validations", () => {
expect(() => gtFive.parse(5)).toThrow();
expect(() => gteFive.parse(4)).toThrow();
expect(() => intNum.parse(3.14)).toThrow();
expect(() => multipleOfFive.parse(14.9)).toThrow();
expect(() => stepSixPointFour.parse(6.41)).toThrow();
});

test("parse NaN", () => {
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/partialBy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// @ts-ignore TS6133
import { expect, test } from "@jest/globals";

import { util } from "../helpers/util";
import * as z from "../index";

const fish = z.object({
name: z.string(),
age: z.number(),
nested: z.object({}),
});

test("partial by type inference", () => {
const optionalNameFish = fish.partialBy({ name: true });
type optionalNameFish = z.infer<typeof optionalNameFish>;
const f1: util.AssertEqual<
optionalNameFish,
{ name?: string | undefined; age: number; nested: {} }
> = true;
f1;
});

test("partial by parse - success", () => {
const nameOptionalFish = fish.partialBy({ name: true });
nameOptionalFish.parse({ age: 0, nested: {} });
});

test("partial by parse - fail", () => {
fish.partialBy({ name: true, age: true }).parse({ nested: {} } as any);
fish
.partialBy({ name: true })
.parse({ name: "bob", age: 12, nested: {} } as any);
fish.partialBy({ name: true }).parse({ age: 12, nested: {} } as any);

const nameOptionalFish = fish.partialBy({ name: true });
const bad1 = () =>
nameOptionalFish.parse({ name: 12, age: 12, nested: {} } as any);
const bad2 = () => nameOptionalFish.parse({ age: 12 } as any);

expect(bad1).toThrow();
expect(bad2).toThrow();
});

0 comments on commit 5f998be

Please sign in to comment.