Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added support multiple parameters in .startsWith() and .endsWith() in string for ticket #3683 #3693

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,8 +735,8 @@ z.string().cuid2();
z.string().ulid();
z.string().regex(regex);
z.string().includes(string);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().startsWith(string | string[]);
z.string().endsWith(string | string[]);
z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed
z.string().ip(); // defaults to allow both IPv4 and IPv6

Expand Down Expand Up @@ -775,7 +775,13 @@ 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().startsWith(["https://", "http://"], {
message: "Must provide a valid URL starting with http:// or https://",
});
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().endsWith([".com", ".org"], {
message: "Only .com or .org domains allowed",
});
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
z.string().date({ message: "Invalid date string!" });
z.string().time({ message: "Invalid time string!" });
Expand Down
10 changes: 8 additions & 2 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,8 @@ z.string().ulid();
z.string().duration();
z.string().regex(regex);
z.string().includes(string);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().startsWith(string | string[]);
z.string().endsWith(string | string[]);
z.string().datetime(); // ISO 8601;默认值为无 UTC 偏移,选项见下文
z.string().ip(); // 默认为 IPv4 和 IPv6,选项见下文

Expand Down Expand Up @@ -517,7 +517,13 @@ 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().startsWith(["https://", "http://"], {
message: "Must provide a valid URL starting with http:// or https://",
});
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().endsWith([".com", ".org"], {
message: "Only .com or .org domains allowed",
});
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
z.string().ip({ message: "Invalid IP address" });
```
Expand Down
15 changes: 11 additions & 4 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
<td align="center">
<p></p>
<p>
<a href="https://speakeasyapi.dev/?utm_source=zod+docs">
<a href="https://speakeasy.com/?utm_source=zod+docs">
<picture height="40px">
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/colinhacks/zod/assets/3084745/b1d86601-c7fb-483c-9927-5dc24ce8b737">
<img alt="speakeasy" height="40px" src="https://github.com/colinhacks/zod/assets/3084745/647524a4-22bb-4199-be70-404207a5a2b5">
Expand All @@ -261,7 +261,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
<br />
SDKs & Terraform providers for your API
<br/>
<a href="https://speakeasyapi.dev/?utm_source=zod+docs" style="text-decoration:none;">speakeasyapi.dev</a>
<a href="https://speakeasy.com/?utm_source=zod+docs" style="text-decoration:none;">speakeasy.com</a>
</p>
<p></p>
</td>
Expand Down Expand Up @@ -490,6 +490,7 @@ There are a growing number of tools that are built atop or support Zod natively!
- [`zod-prisma`](https://github.com/CarterGrimmeisen/zod-prisma): Generate Zod schemas from your Prisma schema.
- [`Supervillain`](https://github.com/Southclaws/supervillain): Generate Zod schemas from your Go structs.
- [`prisma-zod-generator`](https://github.com/omar-dulaimi/prisma-zod-generator): Emit Zod schemas from your Prisma schema.
- [`drizzle-zod`](https://orm.drizzle.team/docs/zod): Emit Zod schemas from your Drizzle schema.
- [`prisma-trpc-generator`](https://github.com/omar-dulaimi/prisma-trpc-generator): Emit fully implemented tRPC routers and their validation schemas using Zod.
- [`zod-prisma-types`](https://github.com/chrishoermann/zod-prisma-types) Create Zod types from your Prisma models.
- [`quicktype`](https://app.quicktype.io/): Convert JSON objects and JSON schemas into Zod schemas.
Expand Down Expand Up @@ -734,8 +735,8 @@ z.string().cuid2();
z.string().ulid();
z.string().regex(regex);
z.string().includes(string);
z.string().startsWith(string);
z.string().endsWith(string);
z.string().startsWith(string | string[]);
z.string().endsWith(string | string[]);
z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed
z.string().ip(); // defaults to allow both IPv4 and IPv6

Expand Down Expand Up @@ -774,7 +775,13 @@ 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().startsWith(["https://", "http://"], {
message: "Must provide a valid URL starting with http:// or https://",
});
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().endsWith([".com", ".org"], {
message: "Only .com or .org domains allowed",
});
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
z.string().date({ message: "Invalid date string!" });
z.string().time({ message: "Invalid time string!" });
Expand Down
6 changes: 6 additions & 0 deletions deno/lib/__tests__/string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ 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 startsWithMultiple = z.string().startsWith(["foo_", "bar_", "baz_"]);
const endsWith = z.string().endsWith("endsWith");
const endsWithMultiple = z.string().endsWith(["foo_", "bar_", "baz_"]);

test("passing validations", () => {
minFive.parse("12345");
Expand All @@ -23,7 +25,9 @@ test("passing validations", () => {
includes.parse("XincludesXX");
includesFromIndex2.parse("XXXincludesXX");
startsWith.parse("startsWithX");
startsWithMultiple.parse("baz_12345");
endsWith.parse("XendsWith");
endsWithMultiple.parse("12345baz_");
});

test("failing validations", () => {
Expand All @@ -35,7 +39,9 @@ test("failing validations", () => {
expect(() => includes.parse("XincludeXX")).toThrow();
expect(() => includesFromIndex2.parse("XincludesXX")).toThrow();
expect(() => startsWith.parse("x")).toThrow();
expect(() => startsWithMultiple.parse("x")).toThrow();
expect(() => endsWith.parse("x")).toThrow();
expect(() => endsWithMultiple.parse("x")).toThrow();
});

test("email validations", () => {
Expand Down
28 changes: 22 additions & 6 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,8 @@ export type ZodStringCheck =
| { kind: "ulid"; message?: string }
| { kind: "startsWith"; value: string; message?: string }
| { kind: "endsWith"; value: string; message?: string }
| { kind: "startsWith"; value: string | string[]; message?: string }
| { kind: "endsWith"; value: string | string[]; message?: string }
| { kind: "regex"; regex: RegExp; message?: string }
| { kind: "trim"; message?: string }
| { kind: "toLowerCase"; message?: string }
Expand Down Expand Up @@ -856,21 +858,35 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
} else if (check.kind === "toUpperCase") {
input.data = input.data.toUpperCase();
} else if (check.kind === "startsWith") {
if (!(input.data as string).startsWith(check.value)) {
if (
(typeof check.value === "string" &&
!(input.data as string).startsWith(check.value)) ||
(Array.isArray(check.value) &&
!check.value.some((e: string) =>
(input.data as string)?.startsWith(e)
))
) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { startsWith: check.value },
validation: { startsWith: check.value.toString() },
message: check.message,
});
status.dirty();
}
} else if (check.kind === "endsWith") {
if (!(input.data as string).endsWith(check.value)) {
if (
(typeof check.value === "string" &&
!(input.data as string).endsWith(check.value)) ||
(Array.isArray(check.value) &&
!check.value.some((e: string) =>
(input.data as string)?.endsWith(e)
))
) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { endsWith: check.value },
validation: { endsWith: check.value.toString() },
message: check.message,
});
status.dirty();
Expand Down Expand Up @@ -1082,15 +1098,15 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
});
}

startsWith(value: string, message?: errorUtil.ErrMessage) {
startsWith(value: string | string[], message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "startsWith",
value: value,
...errorUtil.errToObj(message),
});
}

endsWith(value: string, message?: errorUtil.ErrMessage) {
endsWith(value: string | string[], message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "endsWith",
value: value,
Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ 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 startsWithMultiple = z.string().startsWith(["foo_", "bar_", "baz_"]);
const endsWith = z.string().endsWith("endsWith");
const endsWithMultiple = z.string().endsWith(["foo_", "bar_", "baz_"]);

test("passing validations", () => {
minFive.parse("12345");
Expand All @@ -22,7 +24,9 @@ test("passing validations", () => {
includes.parse("XincludesXX");
includesFromIndex2.parse("XXXincludesXX");
startsWith.parse("startsWithX");
startsWithMultiple.parse("baz_12345");
endsWith.parse("XendsWith");
endsWithMultiple.parse("12345baz_");
});

test("failing validations", () => {
Expand All @@ -34,7 +38,9 @@ test("failing validations", () => {
expect(() => includes.parse("XincludeXX")).toThrow();
expect(() => includesFromIndex2.parse("XincludesXX")).toThrow();
expect(() => startsWith.parse("x")).toThrow();
expect(() => startsWithMultiple.parse("x")).toThrow();
expect(() => endsWith.parse("x")).toThrow();
expect(() => endsWithMultiple.parse("x")).toThrow();
});

test("email validations", () => {
Expand Down
28 changes: 22 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,8 @@ export type ZodStringCheck =
| { kind: "ulid"; message?: string }
| { kind: "startsWith"; value: string; message?: string }
| { kind: "endsWith"; value: string; message?: string }
| { kind: "startsWith"; value: string | string[]; message?: string }
| { kind: "endsWith"; value: string | string[]; message?: string }
| { kind: "regex"; regex: RegExp; message?: string }
| { kind: "trim"; message?: string }
| { kind: "toLowerCase"; message?: string }
Expand Down Expand Up @@ -856,21 +858,35 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
} else if (check.kind === "toUpperCase") {
input.data = input.data.toUpperCase();
} else if (check.kind === "startsWith") {
if (!(input.data as string).startsWith(check.value)) {
if (
(typeof check.value === "string" &&
!(input.data as string).startsWith(check.value)) ||
(Array.isArray(check.value) &&
!check.value.some((e: string) =>
(input.data as string)?.startsWith(e)
))
) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { startsWith: check.value },
validation: { startsWith: check.value.toString() },
message: check.message,
});
status.dirty();
}
} else if (check.kind === "endsWith") {
if (!(input.data as string).endsWith(check.value)) {
if (
(typeof check.value === "string" &&
!(input.data as string).endsWith(check.value)) ||
(Array.isArray(check.value) &&
!check.value.some((e: string) =>
(input.data as string)?.endsWith(e)
))
) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { endsWith: check.value },
validation: { endsWith: check.value.toString() },
message: check.message,
});
status.dirty();
Expand Down Expand Up @@ -1082,15 +1098,15 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
});
}

startsWith(value: string, message?: errorUtil.ErrMessage) {
startsWith(value: string | string[], message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "startsWith",
value: value,
...errorUtil.errToObj(message),
});
}

endsWith(value: string, message?: errorUtil.ErrMessage) {
endsWith(value: string | string[], message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "endsWith",
value: value,
Expand Down