From 342fc3236c12d5fae881b8d33fc609ddcdefb72e Mon Sep 17 00:00:00 2001 From: Lav Kumar Date: Thu, 8 Aug 2024 21:23:51 +0530 Subject: [PATCH] feat: added support for hostname method in z.string() #3589 --- README.md | 2 ++ README_ZH.md | 2 ++ deno/lib/README.md | 7 +++++-- deno/lib/ZodError.ts | 1 + deno/lib/__tests__/string.test.ts | 14 ++++++++++++++ deno/lib/locales/en.ts | 2 ++ deno/lib/types.ts | 31 +++++++++++++++++++++++++++++++ src/ZodError.ts | 1 + src/__tests__/string.test.ts | 14 ++++++++++++++ src/locales/en.ts | 2 ++ src/types.ts | 31 +++++++++++++++++++++++++++++++ 11 files changed, 105 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a5a7c54c2..f7331d97d 100644 --- a/README.md +++ b/README.md @@ -735,6 +735,7 @@ z.string().cuid2(); z.string().ulid(); z.string().regex(regex); z.string().includes(string); +z.string().hostname(); z.string().startsWith(string); z.string().endsWith(string); z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed @@ -774,6 +775,7 @@ 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().hostname({ message: "Must provide a valid hostname" }); 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." }); diff --git a/README_ZH.md b/README_ZH.md index 0c9ab46a0..97dae8447 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -483,6 +483,7 @@ z.string().ulid(); z.string().duration(); z.string().regex(regex); z.string().includes(string); +z.string().hostname(); z.string().startsWith(string); z.string().endsWith(string); z.string().datetime(); // ISO 8601;默认值为无 UTC 偏移,选项见下文 @@ -516,6 +517,7 @@ 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().hostname({ message: "Must provide a valid hostname" }); 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." }); diff --git a/deno/lib/README.md b/deno/lib/README.md index 84ca28ad4..f7331d97d 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -252,7 +252,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod

- + speakeasy @@ -261,7 +261,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
SDKs & Terraform providers for your API
-
speakeasyapi.dev + speakeasy.com

@@ -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. @@ -734,6 +735,7 @@ z.string().cuid2(); z.string().ulid(); z.string().regex(regex); z.string().includes(string); +z.string().hostname(); z.string().startsWith(string); z.string().endsWith(string); z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed @@ -773,6 +775,7 @@ 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().hostname({ message: "Must provide a valid hostname" }); 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." }); diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index e757cd8ba..4fddd2785 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -106,6 +106,7 @@ export type StringValidation = | "base64" | { includes: string; position?: number } | { startsWith: string } + | { hostname: string } | { endsWith: string }; export interface ZodInvalidStringIssue extends ZodIssueBase { diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index b70152750..cc41e8fbd 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -12,6 +12,7 @@ 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"); +const hostname = z.string().hostname(); test("passing validations", () => { minFive.parse("12345"); @@ -24,6 +25,12 @@ test("passing validations", () => { includesFromIndex2.parse("XXXincludesXX"); startsWith.parse("startsWithX"); endsWith.parse("XendsWith"); + hostname.parse("developer.mozilla.org"); + hostname.parse("hello.world.example.com"); + hostname.parse("www.google.com"); + hostname.parse("[2001:db8::ff00:42:8329]"); + hostname.parse("192.168.1.1"); + hostname.parse("xn--d1acj3b"); }); test("failing validations", () => { @@ -36,6 +43,13 @@ test("failing validations", () => { expect(() => includesFromIndex2.parse("XincludesXX")).toThrow(); expect(() => startsWith.parse("x")).toThrow(); expect(() => endsWith.parse("x")).toThrow(); + expect(() => hostname.parse("ht!tp://invalid.com")).toThrow(); + expect(() => hostname.parse("xn--d1acj3b..com")).toThrow(); + expect(() => hostname.parse("ex@mple.com")).toThrow(); + expect(() => hostname.parse("[2001:db8::zzzz]")).toThrow(); + expect(() => hostname.parse("exa mple.com")).toThrow(); + expect(() => hostname.parse("-example.com")).toThrow(); + expect(() => hostname.parse("example..com")).toThrow(); }); test("email validations", () => { diff --git a/deno/lib/locales/en.ts b/deno/lib/locales/en.ts index 0665af275..25fa97a1f 100644 --- a/deno/lib/locales/en.ts +++ b/deno/lib/locales/en.ts @@ -55,6 +55,8 @@ const errorMap: ZodErrorMap = (issue, _ctx) => { } } else if ("startsWith" in issue.validation) { message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } else if ("hostname" in issue.validation) { + message = `Invalid input: must be a valid hostname`; } else if ("endsWith" in issue.validation) { message = `Invalid input: must end with "${issue.validation.endsWith}"`; } else { diff --git a/deno/lib/types.ts b/deno/lib/types.ts index d634e524f..5dc7b884b 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -540,6 +540,7 @@ export type ZodStringCheck = | { kind: "includes"; value: string; position?: number; message?: string } | { kind: "cuid2"; message?: string } | { kind: "ulid"; message?: string } + | { kind: "hostname"; message?: string } | { kind: "startsWith"; value: string; message?: string } | { kind: "endsWith"; value: string; message?: string } | { kind: "regex"; regex: RegExp; message?: string } @@ -855,6 +856,29 @@ export class ZodString extends ZodType { input.data = input.data.toLowerCase(); } else if (check.kind === "toUpperCase") { input.data = input.data.toUpperCase(); + } else if (check.kind === "hostname") { + const domainNameRegex = + /^(?!-)(?!.*--)(?!.*\.\.)(?!.*\.$)[a-zA-Z0-9-]{1,63}(? { }); } + hostname(options?: { message?: string }) { + return this._addCheck({ + kind: "hostname", + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value: string, message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "startsWith", diff --git a/src/ZodError.ts b/src/ZodError.ts index c1f7aa3ee..2803d4fc5 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -106,6 +106,7 @@ export type StringValidation = | "base64" | { includes: string; position?: number } | { startsWith: string } + | { hostname: string } | { endsWith: string }; export interface ZodInvalidStringIssue extends ZodIssueBase { diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index 24f10ee45..93314050f 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -11,6 +11,7 @@ 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"); +const hostname = z.string().hostname(); test("passing validations", () => { minFive.parse("12345"); @@ -23,6 +24,12 @@ test("passing validations", () => { includesFromIndex2.parse("XXXincludesXX"); startsWith.parse("startsWithX"); endsWith.parse("XendsWith"); + hostname.parse("developer.mozilla.org"); + hostname.parse("hello.world.example.com"); + hostname.parse("www.google.com"); + hostname.parse("[2001:db8::ff00:42:8329]"); + hostname.parse("192.168.1.1"); + hostname.parse("xn--d1acj3b"); }); test("failing validations", () => { @@ -35,6 +42,13 @@ test("failing validations", () => { expect(() => includesFromIndex2.parse("XincludesXX")).toThrow(); expect(() => startsWith.parse("x")).toThrow(); expect(() => endsWith.parse("x")).toThrow(); + expect(() => hostname.parse("ht!tp://invalid.com")).toThrow(); + expect(() => hostname.parse("xn--d1acj3b..com")).toThrow(); + expect(() => hostname.parse("ex@mple.com")).toThrow(); + expect(() => hostname.parse("[2001:db8::zzzz]")).toThrow(); + expect(() => hostname.parse("exa mple.com")).toThrow(); + expect(() => hostname.parse("-example.com")).toThrow(); + expect(() => hostname.parse("example..com")).toThrow(); }); test("email validations", () => { diff --git a/src/locales/en.ts b/src/locales/en.ts index 11325a95b..b48ef979d 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -55,6 +55,8 @@ const errorMap: ZodErrorMap = (issue, _ctx) => { } } else if ("startsWith" in issue.validation) { message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } else if ("hostname" in issue.validation) { + message = `Invalid input: must be a valid hostname`; } else if ("endsWith" in issue.validation) { message = `Invalid input: must end with "${issue.validation.endsWith}"`; } else { diff --git a/src/types.ts b/src/types.ts index 0767073c5..f6431046e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -540,6 +540,7 @@ export type ZodStringCheck = | { kind: "includes"; value: string; position?: number; message?: string } | { kind: "cuid2"; message?: string } | { kind: "ulid"; message?: string } + | { kind: "hostname"; message?: string } | { kind: "startsWith"; value: string; message?: string } | { kind: "endsWith"; value: string; message?: string } | { kind: "regex"; regex: RegExp; message?: string } @@ -855,6 +856,29 @@ export class ZodString extends ZodType { input.data = input.data.toLowerCase(); } else if (check.kind === "toUpperCase") { input.data = input.data.toUpperCase(); + } else if (check.kind === "hostname") { + const domainNameRegex = + /^(?!-)(?!.*--)(?!.*\.\.)(?!.*\.$)[a-zA-Z0-9-]{1,63}(? { }); } + hostname(options?: { message?: string }) { + return this._addCheck({ + kind: "hostname", + ...errorUtil.errToObj(options?.message), + }); + } + startsWith(value: string, message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "startsWith",