Skip to content

Commit

Permalink
add .includes(value, options?) @ ZodString. (#1887)
Browse files Browse the repository at this point in the history
* add `.includes(...)` @ `ZodString`.

* update README.md.

* update README.md.

* fix prettier error @ benchmarks primitives.
  • Loading branch information
igalklebanov committed Mar 5, 2023
1 parent 6b8f655 commit 9012dc7
Show file tree
Hide file tree
Showing 10 changed files with 74 additions and 4 deletions.
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -596,6 +596,7 @@ z.string().uuid();
z.string().cuid();
z.string().cuid2();
z.string().regex(regex);
z.string().includes(string);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().datetime(); // defaults to UTC, see below for options
Expand Down Expand Up @@ -628,6 +629,7 @@ z.string().email({ message: "Invalid email address" });
z.string().url({ message: "Invalid url" });
z.string().emoji({ message: "Contains non-emoji characters" });
z.string().uuid({ message: "Invalid UUID" });
z.string().includes("tuna", { message: "Must include tuna" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
Expand Down
2 changes: 2 additions & 0 deletions deno/lib/README.md
Expand Up @@ -596,6 +596,7 @@ z.string().uuid();
z.string().cuid();
z.string().cuid2();
z.string().regex(regex);
z.string().includes(string);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().trim(); // trim whitespace
Expand Down Expand Up @@ -627,6 +628,7 @@ z.string().email({ message: "Invalid email address" });
z.string().url({ message: "Invalid url" });
z.string().emoji({ message: "Contains non-emoji characters" });
z.string().uuid({ message: "Invalid UUID" });
z.string().includes("tuna", { message: "Must include tuna" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
Expand Down
1 change: 1 addition & 0 deletions deno/lib/ZodError.ts
Expand Up @@ -98,6 +98,7 @@ export type StringValidation =
| "cuid2"
| "datetime"
| "ip"
| { includes: string; position?: number }
| { startsWith: string }
| { endsWith: string };

Expand Down
8 changes: 7 additions & 1 deletion deno/lib/__tests__/string.test.ts
Expand Up @@ -7,7 +7,9 @@ import * as z from "../index.ts";
const minFive = z.string().min(5, "min5");
const maxFive = z.string().max(5, "max5");
const justFive = z.string().length(5);
const nonempty = z.string().nonempty("nonempty");
const nonempty = z.string().min(1, "nonempty");
const includes = z.string().includes("includes");
const includesFromIndex2 = z.string().includes("includes", { position: 2 });
const startsWith = z.string().startsWith("startsWith");
const endsWith = z.string().endsWith("endsWith");

Expand All @@ -18,6 +20,8 @@ test("passing validations", () => {
maxFive.parse("1234");
nonempty.parse("1");
justFive.parse("12345");
includes.parse("XincludesXX");
includesFromIndex2.parse("XXXincludesXX");
startsWith.parse("startsWithX");
endsWith.parse("XendsWith");
});
Expand All @@ -28,6 +32,8 @@ test("failing validations", () => {
expect(() => nonempty.parse("")).toThrow();
expect(() => justFive.parse("1234")).toThrow();
expect(() => justFive.parse("123456")).toThrow();
expect(() => includes.parse("XincludeXX")).toThrow();
expect(() => includesFromIndex2.parse("XincludesXX")).toThrow();
expect(() => startsWith.parse("x")).toThrow();
expect(() => endsWith.parse("x")).toThrow();
});
Expand Down
8 changes: 7 additions & 1 deletion deno/lib/locales/en.ts
Expand Up @@ -47,7 +47,13 @@ const errorMap: ZodErrorMap = (issue, _ctx) => {
break;
case ZodIssueCode.invalid_string:
if (typeof issue.validation === "object") {
if ("startsWith" in issue.validation) {
if ("includes" in issue.validation) {
message = `Invalid input: must include "${issue.validation.includes}"`;

if (typeof issue.validation.position === "number") {
message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`;
}
} else if ("startsWith" in issue.validation) {
message = `Invalid input: must start with "${issue.validation.startsWith}"`;
} else if ("endsWith" in issue.validation) {
message = `Invalid input: must end with "${issue.validation.endsWith}"`;
Expand Down
20 changes: 20 additions & 0 deletions deno/lib/types.ts
Expand Up @@ -496,6 +496,7 @@ export type ZodStringCheck =
| { kind: "emoji"; message?: string }
| { kind: "uuid"; message?: string }
| { kind: "cuid"; message?: string }
| { kind: "includes"; value: string; position?: number; message?: string }
| { kind: "cuid2"; message?: string }
| { kind: "startsWith"; value: string; message?: string }
| { kind: "endsWith"; value: string; message?: string }
Expand Down Expand Up @@ -736,6 +737,16 @@ export class ZodString extends ZodType<string, ZodStringDef> {
}
} else if (check.kind === "trim") {
input.data = input.data.trim();
} else if (check.kind === "includes") {
if (!(input.data as string).includes(check.value, check.position)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { includes: check.value, position: check.position },
message: check.message,
});
status.dirty();
}
} else if (check.kind === "toLowerCase") {
input.data = input.data.toLowerCase();
} else if (check.kind === "toUpperCase") {
Expand Down Expand Up @@ -865,6 +876,15 @@ export class ZodString extends ZodType<string, ZodStringDef> {
});
}

includes(value: string, options?: { message?: string; position?: number }) {
return this._addCheck({
kind: "includes",
value: value,
position: options?.position,
...errorUtil.errToObj(options?.message),
});
}

startsWith(value: string, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "startsWith",
Expand Down
1 change: 1 addition & 0 deletions src/ZodError.ts
Expand Up @@ -98,6 +98,7 @@ export type StringValidation =
| "cuid2"
| "datetime"
| "ip"
| { includes: string; position?: number }
| { startsWith: string }
| { endsWith: string };

Expand Down
8 changes: 7 additions & 1 deletion src/__tests__/string.test.ts
Expand Up @@ -6,7 +6,9 @@ import * as z from "../index";
const minFive = z.string().min(5, "min5");
const maxFive = z.string().max(5, "max5");
const justFive = z.string().length(5);
const nonempty = z.string().nonempty("nonempty");
const nonempty = z.string().min(1, "nonempty");
const includes = z.string().includes("includes");
const includesFromIndex2 = z.string().includes("includes", { position: 2 });
const startsWith = z.string().startsWith("startsWith");
const endsWith = z.string().endsWith("endsWith");

Expand All @@ -17,6 +19,8 @@ test("passing validations", () => {
maxFive.parse("1234");
nonempty.parse("1");
justFive.parse("12345");
includes.parse("XincludesXX");
includesFromIndex2.parse("XXXincludesXX");
startsWith.parse("startsWithX");
endsWith.parse("XendsWith");
});
Expand All @@ -27,6 +31,8 @@ test("failing validations", () => {
expect(() => nonempty.parse("")).toThrow();
expect(() => justFive.parse("1234")).toThrow();
expect(() => justFive.parse("123456")).toThrow();
expect(() => includes.parse("XincludeXX")).toThrow();
expect(() => includesFromIndex2.parse("XincludesXX")).toThrow();
expect(() => startsWith.parse("x")).toThrow();
expect(() => endsWith.parse("x")).toThrow();
});
Expand Down
8 changes: 7 additions & 1 deletion src/locales/en.ts
Expand Up @@ -47,7 +47,13 @@ const errorMap: ZodErrorMap = (issue, _ctx) => {
break;
case ZodIssueCode.invalid_string:
if (typeof issue.validation === "object") {
if ("startsWith" in issue.validation) {
if ("includes" in issue.validation) {
message = `Invalid input: must include "${issue.validation.includes}"`;

if (typeof issue.validation.position === "number") {
message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`;
}
} else if ("startsWith" in issue.validation) {
message = `Invalid input: must start with "${issue.validation.startsWith}"`;
} else if ("endsWith" in issue.validation) {
message = `Invalid input: must end with "${issue.validation.endsWith}"`;
Expand Down
20 changes: 20 additions & 0 deletions src/types.ts
Expand Up @@ -496,6 +496,7 @@ export type ZodStringCheck =
| { kind: "emoji"; message?: string }
| { kind: "uuid"; message?: string }
| { kind: "cuid"; message?: string }
| { kind: "includes"; value: string; position?: number; message?: string }
| { kind: "cuid2"; message?: string }
| { kind: "startsWith"; value: string; message?: string }
| { kind: "endsWith"; value: string; message?: string }
Expand Down Expand Up @@ -736,6 +737,16 @@ export class ZodString extends ZodType<string, ZodStringDef> {
}
} else if (check.kind === "trim") {
input.data = input.data.trim();
} else if (check.kind === "includes") {
if (!(input.data as string).includes(check.value, check.position)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { includes: check.value, position: check.position },
message: check.message,
});
status.dirty();
}
} else if (check.kind === "toLowerCase") {
input.data = input.data.toLowerCase();
} else if (check.kind === "toUpperCase") {
Expand Down Expand Up @@ -865,6 +876,15 @@ export class ZodString extends ZodType<string, ZodStringDef> {
});
}

includes(value: string, options?: { message?: string; position?: number }) {
return this._addCheck({
kind: "includes",
value: value,
position: options?.position,
...errorUtil.errToObj(options?.message),
});
}

startsWith(value: string, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "startsWith",
Expand Down

0 comments on commit 9012dc7

Please sign in to comment.