Skip to content

Commit

Permalink
Merge pull request #602 from colinhacks/3.8
Browse files Browse the repository at this point in the history
v3.8.0
  • Loading branch information
colinhacks authored Aug 23, 2021
2 parents 30b453b + f74c481 commit 7fe864e
Show file tree
Hide file tree
Showing 16 changed files with 321 additions and 22 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

### 3.8

- Add `z.preprocess`
- Implement CUID validation on ZodString (`z.string().cuid()`)
- Improved `.deepPartial()`: now recursively operates on arrays, tuples, optionals, and nullables (in addition to objects)

### 3.7

- Eliminate `ZodNonEmptyArray`, add `Cardinality` to `ZodArray`
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ z.string().length(5);
z.string().email();
z.string().url();
z.string().uuid();
z.string().cuid();
z.string().regex(regex);

// deprecated, equivalent to .min(1)
Expand Down Expand Up @@ -483,6 +484,7 @@ const user = z.object({
latitude: z.number(),
longitude: z.number(),
}),
strings: z.array(z.object({ value: z.string() })),
});

const deepPartialUser = user.deepPartial();
Expand All @@ -493,12 +495,13 @@ const deepPartialUser = user.deepPartial();
location?: {
latitude?: number | undefined;
longitude?: number | undefined;
} | undefined
} | undefined,
strings?: { value?: string}[]
}
*/
```

> Important limitation: deep partials only work as expected in direct hierarchies of object schemas. A nested object schema can't be optional, nullable, contain refinements, contain transforms, etc.
> Important limitation: deep partials only work as expected in hierarchies of objects, arrays, and tuples.
#### Unrecognized keys

Expand Down
2 changes: 1 addition & 1 deletion coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion deno/lib/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface ZodInvalidDateIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_date;
}

export type StringValidation = "email" | "url" | "uuid" | "regex";
export type StringValidation = "email" | "url" | "uuid" | "regex" | "cuid";

export interface ZodInvalidStringIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_string;
Expand Down
52 changes: 52 additions & 0 deletions deno/lib/__tests__/partials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const test = Deno.test;

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

const nested = z.object({
name: z.string(),
Expand Down Expand Up @@ -82,6 +83,57 @@ test("deep partial runtime tests", () => {
});
});

test("deep partial optional/nullable", () => {
const schema = z
.object({
name: z.string().optional(),
age: z.number().nullable(),
})
.deepPartial();

expect(schema.shape.name.unwrap()).toBeInstanceOf(ZodOptional);
expect(schema.shape.age.unwrap()).toBeInstanceOf(ZodNullable);
});

test("deep partial tuple", () => {
const schema = z
.object({
tuple: z.tuple([
z.object({
name: z.string().optional(),
age: z.number().nullable(),
}),
]),
})
.deepPartial();

expect(schema.shape.tuple.unwrap().items[0].shape.name).toBeInstanceOf(
ZodOptional
);
});

test("deep partial inference", () => {
const mySchema = z.object({
name: z.string(),
array: z.array(z.object({ asdf: z.string() })),
tuple: z.tuple([z.object({ value: z.string() })]),
});

const partialed = mySchema.deepPartial();
type partialed = z.infer<typeof partialed>;
type expected = {
name?: string | undefined;
array?:
| {
asdf?: string | undefined;
}[]
| undefined;
tuple?: [{ value?: string }] | undefined;
};
const f1: util.AssertEqual<expected, partialed> = true;
f1;
});

test("required", () => {
const object = z.object({
name: z.string(),
Expand Down
10 changes: 10 additions & 0 deletions deno/lib/__tests__/string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ test("bad uuid", () => {
}
});

test("cuid", () => {
const cuid = z.string().cuid();
cuid.parse("ckopqwooh000001la8mbi2im9");
const result = cuid.safeParse("cifjhdsfhsd-invalid-cuid");
expect(result.success).toEqual(false);
if (!result.success) {
expect(result.error.issues[0].message).toEqual("Invalid cuid");
}
});

test("regex", () => {
z.string()
.regex(/^moo+$/)
Expand Down
7 changes: 7 additions & 0 deletions deno/lib/__tests__/transformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,10 @@ test("multiple transformers", () => {
});
expect(doubler.parse("5")).toEqual(10);
});

test("preprocess", () => {
const schema = z.preprocess((data) => [data], z.string().array());

const value = schema.parse("asdf");
expect(value).toEqual(["asdf"]);
});
24 changes: 23 additions & 1 deletion deno/lib/helpers/partialUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { ZodArray, ZodObject, ZodOptional, ZodTypeAny } from "../index.ts";
import type {
ZodArray,
ZodNullable,
ZodObject,
ZodOptional,
ZodTuple,
ZodTupleItems,
ZodTypeAny,
} from "../index.ts";

export namespace partialUtil {
// export type DeepPartial<T extends AnyZodObject> = T extends AnyZodObject
Expand Down Expand Up @@ -38,6 +46,20 @@ export namespace partialUtil {
>
: T extends ZodArray<infer Type, infer Card>
? ZodArray<DeepPartial<Type>, Card>
: T extends ZodOptional<infer Type>
? ZodOptional<DeepPartial<Type>>
: T extends ZodNullable<infer Type>
? ZodNullable<DeepPartial<Type>>
: T extends ZodTuple<infer Items>
? {
[k in keyof Items]: Items[k] extends ZodTypeAny
? DeepPartial<Items[k]>
: never;
} extends infer PI
? PI extends ZodTupleItems
? ZodTuple<PI>
: never
: never
: T;
// {
// // optional: T extends ZodOptional<ZodTypeAny> ? T : ZodOptional<T>;
Expand Down
68 changes: 61 additions & 7 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,15 @@ type ZodStringCheck =
| { kind: "email"; message?: string }
| { kind: "url"; message?: string }
| { kind: "uuid"; message?: string }
| { kind: "cuid"; message?: string }
| { kind: "regex"; regex: RegExp; message?: string };

export interface ZodStringDef extends ZodTypeDef {
checks: ZodStringCheck[];
typeName: ZodFirstPartyTypeKind.ZodString;
}

const cuidRegex = /^c[^\s-]{8,}$/i;
const uuidRegex = /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i;
// from https://stackoverflow.com/a/46181/1550155
// old version: too slow, didn't support unicode
Expand Down Expand Up @@ -386,6 +388,15 @@ export class ZodString extends ZodType<string, ZodStringDef> {
message: check.message,
});
}
} else if (check.kind === "cuid") {
if (!cuidRegex.test(data)) {
invalid = true;
ctx.addIssue(data, {
validation: "cuid",
code: ZodIssueCode.invalid_string,
message: check.message,
});
}
} else if (check.kind === "url") {
try {
new URL(data);
Expand Down Expand Up @@ -450,6 +461,15 @@ export class ZodString extends ZodType<string, ZodStringDef> {
],
});

cuid = (message?: errorUtil.ErrMessage) =>
new ZodString({
...this._def,
checks: [
...this._def.checks,
{ kind: "cuid", ...errorUtil.errToObj(message) },
],
});

regex = (regex: RegExp, message?: errorUtil.ErrMessage) =>
new ZodString({
...this._def,
Expand Down Expand Up @@ -1388,6 +1408,14 @@ function deepPartialify(schema: ZodTypeAny): any {
}) as any;
} else if (schema instanceof ZodArray) {
return ZodArray.create(deepPartialify(schema.element));
} else if (schema instanceof ZodOptional) {
return ZodOptional.create(deepPartialify(schema.unwrap()));
} else if (schema instanceof ZodNullable) {
return ZodNullable.create(deepPartialify(schema.unwrap()));
} else if (schema instanceof ZodTuple) {
return ZodTuple.create(
schema.items.map((item: any) => deepPartialify(item))
);
} else {
return schema;
}
Expand Down Expand Up @@ -1902,17 +1930,17 @@ export class ZodIntersection<
////////// //////////
////////////////////////////////////////
////////////////////////////////////////
export type OutputTypeOfTuple<T extends [ZodTypeAny, ...ZodTypeAny[]] | []> = {
export type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]];
export type OutputTypeOfTuple<T extends ZodTupleItems | []> = {
[k in keyof T]: T[k] extends ZodType<any, any> ? T[k]["_output"] : never;
};

export type InputTypeOfTuple<T extends [ZodTypeAny, ...ZodTypeAny[]] | []> = {
export type InputTypeOfTuple<T extends ZodTupleItems | []> = {
[k in keyof T]: T[k] extends ZodType<any, any> ? T[k]["_input"] : never;
};

export interface ZodTupleDef<
T extends [ZodTypeAny, ...ZodTypeAny[]] | [] = [ZodTypeAny, ...ZodTypeAny[]]
> extends ZodTypeDef {
export interface ZodTupleDef<T extends ZodTupleItems | [] = ZodTupleItems>
extends ZodTypeDef {
items: T;
typeName: ZodFirstPartyTypeKind.ZodTuple;
}
Expand Down Expand Up @@ -2701,6 +2729,7 @@ export interface ZodEffectsDef<T extends ZodTypeAny = ZodTypeAny>
extends ZodTypeDef {
schema: T;
typeName: ZodFirstPartyTypeKind.ZodEffects;
preprocess?: Mod<any>;
effects?: Effect<any>[];
}

Expand All @@ -2714,11 +2743,20 @@ export class ZodEffects<

_parse(
ctx: ParseContext,
data: any,
parsedType: ZodParsedType
initialData: any,
initialParsedType: ZodParsedType
): ParseReturnType<Output> {
const isSync = ctx.params.async === false;
const preprocess = this._def.preprocess;
const effects = this._def.effects || [];

let data = initialData;
let parsedType: ZodParsedType = initialParsedType;
if (preprocess) {
data = preprocess.transform(initialData);
parsedType = getParsedType(data);
}

const checkCtx: RefinementCtx = {
issueFound: false,
addIssue: function (arg: MakeErrorData) {
Expand All @@ -2729,6 +2767,7 @@ export class ZodEffects<
return pathToArray(ctx.path);
},
};

checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx);

let invalid = false;
Expand Down Expand Up @@ -2826,6 +2865,19 @@ export class ZodEffects<

return newTx;
};

static createWithPreprocess = <I extends ZodTypeAny>(
preprocess: (arg: unknown) => unknown,
schema: I
): ZodEffects<I, I["_output"]> => {
const newTx = new ZodEffects({
schema,
preprocess: { type: "transform", transform: preprocess },
typeName: ZodFirstPartyTypeKind.ZodEffects,
});

return newTx;
};
}

export { ZodEffects as ZodTransformer };
Expand Down Expand Up @@ -3070,6 +3122,7 @@ const promiseType = ZodPromise.create;
const effectsType = ZodEffects.create;
const optionalType = ZodOptional.create;
const nullableType = ZodNullable.create;
const preprocessType = ZodEffects.createWithPreprocess;
const ostring = () => stringType().optional();
const onumber = () => numberType().optional();
const oboolean = () => booleanType().optional();
Expand Down Expand Up @@ -3098,6 +3151,7 @@ export {
onumber,
optionalType as optional,
ostring,
preprocessType as preprocess,
promiseType as promise,
recordType as record,
setType as set,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zod",
"version": "3.7.3",
"version": "3.8.0",
"description": "TypeScript-first schema declaration and validation library with static type inference",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface ZodInvalidDateIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_date;
}

export type StringValidation = "email" | "url" | "uuid" | "regex";
export type StringValidation = "email" | "url" | "uuid" | "regex" | "cuid";

export interface ZodInvalidStringIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_string;
Expand Down
Loading

0 comments on commit 7fe864e

Please sign in to comment.