Skip to content

Commit

Permalink
fix: make sure only mask keys with truthy values are respected at run…
Browse files Browse the repository at this point in the history
…time @ `.pick`, `.omit`, `.partial` & `.required`. (#1875)

* make sure only mask keys with truthy value are respected at runtime.

* ignore falsy values test cases for `.partial(mask)` & `.required(mask)`.

Co-authored-by: Max Arturo <5713763+maxArturo@users.noreply.github.com>

* fix prettier error @ benchmarks primitives.

---------

Co-authored-by: Max Arturo <5713763+maxArturo@users.noreply.github.com>
  • Loading branch information
igalklebanov and maxArturo committed Feb 8, 2023
1 parent e4b9ac8 commit 8fcdcd6
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 99 deletions.
52 changes: 45 additions & 7 deletions deno/lib/__tests__/partials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,21 +185,59 @@ test("required with mask", () => {
expect(requiredObject.shape.country).toBeInstanceOf(z.ZodOptional);
});

test("required with mask -- ignore falsy values", () => {
const object = z.object({
name: z.string(),
age: z.number().optional(),
field: z.string().optional().default("asdf"),
country: z.string().optional(),
});

// @ts-expect-error
const requiredObject = object.required({ age: true, country: false });
expect(requiredObject.shape.name).toBeInstanceOf(z.ZodString);
expect(requiredObject.shape.age).toBeInstanceOf(z.ZodNumber);
expect(requiredObject.shape.field).toBeInstanceOf(z.ZodDefault);
expect(requiredObject.shape.country).toBeInstanceOf(z.ZodOptional);
});

test("partial with mask", async () => {
const object = z.object({
name: z.string(),
age: z.number().optional(),
field: z.string().optional().default("asdf"),
country: z.string(),
});

const masked = object
.partial({
name: true,
age: true,
field: true,
})
.partial({ age: true, field: true, name: true })
.strict();

masked.parse({});
await masked.parseAsync({});
expect(masked.shape.name).toBeInstanceOf(z.ZodOptional);
expect(masked.shape.age).toBeInstanceOf(z.ZodOptional);
expect(masked.shape.field).toBeInstanceOf(z.ZodOptional);
expect(masked.shape.country).toBeInstanceOf(z.ZodString);

masked.parse({ country: "US" });
await masked.parseAsync({ country: "US" });
});

test("partial with mask -- ignore falsy values", async () => {
const object = z.object({
name: z.string(),
age: z.number().optional(),
field: z.string().optional().default("asdf"),
country: z.string(),
});

// @ts-expect-error
const masked = object.partial({ name: true, country: false }).strict();

expect(masked.shape.name).toBeInstanceOf(z.ZodOptional);
expect(masked.shape.age).toBeInstanceOf(z.ZodOptional);
expect(masked.shape.field).toBeInstanceOf(z.ZodDefault);
expect(masked.shape.country).toBeInstanceOf(z.ZodString);

masked.parse({ country: "US" });
await masked.parseAsync({ country: "US" });
});
22 changes: 20 additions & 2 deletions deno/lib/__tests__/pickomit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ test("pick type inference", () => {
test("pick parse - success", () => {
const nameonlyFish = fish.pick({ name: true });
nameonlyFish.parse({ name: "bob" });

// @ts-expect-error checking runtime picks `name` only.
const anotherNameonlyFish = fish.pick({ name: true, age: false });
anotherNameonlyFish.parse({ name: "bob" });
});

test("pick parse - fail", () => {
Expand All @@ -32,9 +36,14 @@ test("pick parse - fail", () => {
const bad2 = () => nameonlyFish.parse({ name: "bob", age: 12 } as any);
const bad3 = () => nameonlyFish.parse({ age: 12 } as any);

// @ts-expect-error checking runtime picks `name` only.
const anotherNameonlyFish = fish.pick({ name: true, age: false }).strict();
const bad4 = () => anotherNameonlyFish.parse({ name: "bob", age: 12 } as any);

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

test("omit type inference", () => {
Expand All @@ -46,6 +55,10 @@ test("omit type inference", () => {
test("omit parse - success", () => {
const nonameFish = fish.omit({ name: true });
nonameFish.parse({ age: 12, nested: {} });

// @ts-expect-error checking runtime omits `name` only.
const anotherNonameFish = fish.omit({ name: true, age: false });
anotherNonameFish.parse({ age: 12, nested: {} });
});

test("omit parse - fail", () => {
Expand All @@ -54,9 +67,14 @@ test("omit parse - fail", () => {
const bad2 = () => nonameFish.parse({ age: 12 } as any);
const bad3 = () => nonameFish.parse({} as any);

// @ts-expect-error checking runtime omits `name` only.
const anotherNonameFish = fish.omit({ name: true, age: false });
const bad4 = () => anotherNonameFish.parse({ nested: {} } as any);

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

test("nonstrict inference", () => {
Expand All @@ -66,13 +84,13 @@ test("nonstrict inference", () => {
});

test("nonstrict parsing - pass", () => {
const laxfish = fish.nonstrict().pick({ name: true });
const laxfish = fish.passthrough().pick({ name: true });
laxfish.parse({ name: "asdf", whatever: "asdf" });
laxfish.parse({ name: "asdf", age: 12, nested: {} });
});

test("nonstrict parsing - fail", () => {
const laxfish = fish.nonstrict().pick({ name: true });
const laxfish = fish.passthrough().pick({ name: true });
const bad = () => laxfish.parse({ whatever: "asdf" } as any);
expect(bad).toThrow();
});
Expand Down
16 changes: 13 additions & 3 deletions deno/lib/benchmarks/primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ numberSuite
const dateSuite = new Benchmark.Suite("z.date");

const plainDate = z.date();
const minMaxDate = z.date().min(new Date("2021-01-01")).max(new Date("2030-01-01"));
const minMaxDate = z
.date()
.min(new Date("2021-01-01"))
.max(new Date("2030-01-01"));

dateSuite
.add("valid", () => {
Expand Down Expand Up @@ -112,7 +115,7 @@ const symbolSchema = z.symbol();

symbolSuite
.add("valid", () => {
symbolSchema.parse(val.symbol)
symbolSchema.parse(val.symbol);
})
.add("invalid", () => {
try {
Expand All @@ -124,5 +127,12 @@ symbolSuite
});

export default {
suites: [enumSuite, undefinedSuite, literalSuite, numberSuite, dateSuite, symbolSuite],
suites: [
enumSuite,
undefinedSuite,
literalSuite,
numberSuite,
dateSuite,
symbolSuite,
],
};
65 changes: 27 additions & 38 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ const uuidRegex =
// old version: too slow, didn't support unicode
// const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
// eslint-disable-next-line
const emailRegex =
export const emailRegex =
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@((?!-)([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{1,})[^-<>()[\].,;:\s@"]$/i;

// interface IsDateStringOptions extends StringDateOptions {
Expand Down Expand Up @@ -2169,10 +2169,13 @@ export class ZodObject<
mask: Mask
): ZodObject<Pick<T, Extract<keyof T, keyof Mask>>, UnknownKeys, Catchall> {
const shape: any = {};
util.objectKeys(mask).map((key) => {
// only add to shape if key corresponds to an element of the current shape
if (this.shape[key]) shape[key] = this.shape[key];

util.objectKeys(mask).forEach((key) => {
if (mask[key] && this.shape[key]) {
shape[key] = this.shape[key];
}
});

return new ZodObject({
...this._def,
shape: () => shape,
Expand All @@ -2183,11 +2186,13 @@ export class ZodObject<
mask: Mask
): ZodObject<Omit<T, keyof Mask>, UnknownKeys, Catchall> {
const shape: any = {};
util.objectKeys(this.shape).map((key) => {
if (util.objectKeys(mask).indexOf(key) === -1) {

util.objectKeys(this.shape).forEach((key) => {
if (!mask[key]) {
shape[key] = this.shape[key];
}
});

return new ZodObject({
...this._def,
shape: () => shape,
Expand All @@ -2214,24 +2219,16 @@ export class ZodObject<
>;
partial(mask?: any) {
const newShape: any = {};
if (mask) {
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;
} else {
for (const key in this.shape) {
const fieldSchema = this.shape[key];

util.objectKeys(this.shape).forEach((key) => {
const fieldSchema = this.shape[key];

if (mask && !mask[key]) {
newShape[key] = fieldSchema;
} else {
newShape[key] = fieldSchema.optional();
}
}
});

return new ZodObject({
...this._def,
Expand All @@ -2255,30 +2252,22 @@ export class ZodObject<
>;
required(mask?: any) {
const newShape: any = {};
if (mask) {
util.objectKeys(this.shape).map((key) => {
if (util.objectKeys(mask).indexOf(key) === -1) {
newShape[key] = this.shape[key];
} else {
const fieldSchema = this.shape[key];
let newField = fieldSchema;
while (newField instanceof ZodOptional) {
newField = (newField as ZodOptional<any>)._def.innerType;
}
newShape[key] = newField;
}
});
} else {
for (const key in this.shape) {

util.objectKeys(this.shape).forEach((key) => {
if (mask && !mask[key]) {
newShape[key] = this.shape[key];
} else {
const fieldSchema = this.shape[key];
let newField = fieldSchema;

while (newField instanceof ZodOptional) {
newField = (newField as ZodOptional<any>)._def.innerType;
}

newShape[key] = newField;
}
}
});

return new ZodObject({
...this._def,
shape: () => newShape,
Expand Down
52 changes: 45 additions & 7 deletions src/__tests__/partials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,21 +184,59 @@ test("required with mask", () => {
expect(requiredObject.shape.country).toBeInstanceOf(z.ZodOptional);
});

test("required with mask -- ignore falsy values", () => {
const object = z.object({
name: z.string(),
age: z.number().optional(),
field: z.string().optional().default("asdf"),
country: z.string().optional(),
});

// @ts-expect-error
const requiredObject = object.required({ age: true, country: false });
expect(requiredObject.shape.name).toBeInstanceOf(z.ZodString);
expect(requiredObject.shape.age).toBeInstanceOf(z.ZodNumber);
expect(requiredObject.shape.field).toBeInstanceOf(z.ZodDefault);
expect(requiredObject.shape.country).toBeInstanceOf(z.ZodOptional);
});

test("partial with mask", async () => {
const object = z.object({
name: z.string(),
age: z.number().optional(),
field: z.string().optional().default("asdf"),
country: z.string(),
});

const masked = object
.partial({
name: true,
age: true,
field: true,
})
.partial({ age: true, field: true, name: true })
.strict();

masked.parse({});
await masked.parseAsync({});
expect(masked.shape.name).toBeInstanceOf(z.ZodOptional);
expect(masked.shape.age).toBeInstanceOf(z.ZodOptional);
expect(masked.shape.field).toBeInstanceOf(z.ZodOptional);
expect(masked.shape.country).toBeInstanceOf(z.ZodString);

masked.parse({ country: "US" });
await masked.parseAsync({ country: "US" });
});

test("partial with mask -- ignore falsy values", async () => {
const object = z.object({
name: z.string(),
age: z.number().optional(),
field: z.string().optional().default("asdf"),
country: z.string(),
});

// @ts-expect-error
const masked = object.partial({ name: true, country: false }).strict();

expect(masked.shape.name).toBeInstanceOf(z.ZodOptional);
expect(masked.shape.age).toBeInstanceOf(z.ZodOptional);
expect(masked.shape.field).toBeInstanceOf(z.ZodDefault);
expect(masked.shape.country).toBeInstanceOf(z.ZodString);

masked.parse({ country: "US" });
await masked.parseAsync({ country: "US" });
});
Loading

0 comments on commit 8fcdcd6

Please sign in to comment.