From d8e4799076df1957be53cf063198cff56bfd8d24 Mon Sep 17 00:00:00 2001 From: "Matt R. Wilson" Date: Fri, 23 Feb 2024 12:53:03 -0700 Subject: [PATCH 1/3] feat: Add support for ISO-8601 Durations https://en.wikipedia.org/wiki/ISO_8601#Durations As an extension of the ISO standard, the format is also used in RFC 3339, XML Schema Part 2, TC39's Temporal proposal, and a format for JSON Schema strings since draft 2019-09. --- README.md | 3 ++- README_ZH.md | 1 + deno/lib/README.md | 6 ++++++ deno/lib/ZodError.ts | 4 ++++ deno/lib/__tests__/string.test.ts | 14 ++++++++++++++ deno/lib/types.ts | 29 +++++++++++++++++++++++++++++ src/ZodError.ts | 1 + src/__tests__/string.test.ts | 13 +++++++++++++ src/types.ts | 21 +++++++++++++++++++++ 9 files changed, 91 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 584af36f0..97cf7a725 100644 --- a/README.md +++ b/README.md @@ -858,10 +858,11 @@ z.string().endsWith(string); z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed z.string().date(); // ISO date format (YYYY-MM-DD) z.string().time(); // ISO time format (HH:mm:ss[.SSSSSS]) +z.string().duration(); // ISO 8601 duration z.string().ip(); // defaults to allow both IPv4 and IPv6 z.string().base64(); -// transformations +// transforms z.string().trim(); // trim whitespace z.string().toLowerCase(); // toLowerCase z.string().toUpperCase(); // toUpperCase diff --git a/README_ZH.md b/README_ZH.md index 1ace66923..ad19b414a 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -479,6 +479,7 @@ z.string().uuid(); z.string().cuid(); z.string().cuid2(); z.string().ulid(); +z.string().duration(); z.string().regex(regex); z.string().includes(string); z.string().startsWith(string); diff --git a/deno/lib/README.md b/deno/lib/README.md index 584af36f0..125a56bc1 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -855,11 +855,17 @@ z.string().regex(regex); z.string().includes(string); z.string().startsWith(string); z.string().endsWith(string); +<<<<<<< HEAD z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed z.string().date(); // ISO date format (YYYY-MM-DD) z.string().time(); // ISO time format (HH:mm:ss[.SSSSSS]) z.string().ip(); // defaults to allow both IPv4 and IPv6 z.string().base64(); +======= +z.string().datetime(); // ISO 8601; default is without UTC offset, see below for options +z.string().duration(); // ISO 8601 Duration +z.string().ip(); // defaults to IPv4 and IPv6, see below for options +>>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) // transformations z.string().trim(); // trim whitespace diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index 3863c3e56..f587e0e99 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -99,8 +99,12 @@ export type StringValidation = | "cuid2" | "ulid" | "datetime" +<<<<<<< HEAD | "date" | "time" +======= + | "duration" +>>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) | "ip" | "base64" | { includes: string; position?: number } diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index d701be5f0..0a8a8d081 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -548,6 +548,7 @@ test("datetime parsing", () => { ).toThrow(); }); +<<<<<<< HEAD test("date", () => { const a = z.string().date(); expect(a.isDate).toEqual(true); @@ -669,6 +670,19 @@ test("time parsing", () => { // expect(() => time4.parse("00:00:00.0")).toThrow(); // expect(() => time4.parse("00:00:00.000")).toThrow(); // expect(() => time4.parse("00:00:00.000+00:00")).toThrow(); +======= +test("duration", () => { + const duration = z.string().duration(); + expect(duration.isDuration).toEqual(true); + + duration.parse("P3Y6M4DT12H30M5S"); + + const result = duration.safeParse("invalidDuration"); + expect(result.success).toEqual(false); + if (!result.success) { + expect(result.error.issues[0].message).toEqual("Invalid duration"); + } +>>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) }); test("IP validation", () => { diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 0e574c8fb..7b65ff00b 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -553,6 +553,7 @@ export type ZodStringCheck = precision: number | null; message?: string; } +<<<<<<< HEAD | { kind: "date"; // withDate: true; @@ -565,6 +566,10 @@ export type ZodStringCheck = } | { kind: "ip"; version?: IpVersion; message?: string } | { kind: "base64"; message?: string }; +======= + | { kind: "duration"; message?: string } + | { kind: "ip"; version?: IpVersion; message?: string }; +>>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) export interface ZodStringDef extends ZodTypeDef { checks: ZodStringCheck[]; @@ -579,7 +584,13 @@ const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/; // /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +<<<<<<< HEAD const nanoidRegex = /^[a-z0-9_-]{21}$/i; +======= +const durationRegex = + /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/; + +>>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) // from https://stackoverflow.com/a/46181/1550155 // 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; @@ -882,6 +893,7 @@ export class ZodString extends ZodType { }); status.dirty(); } +<<<<<<< HEAD } else if (check.kind === "date") { const regex = dateRegex; @@ -902,6 +914,14 @@ export class ZodString extends ZodType { addIssueToContext(ctx, { code: ZodIssueCode.invalid_string, validation: "time", +======= + } else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, +>>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) message: check.message, }); status.dirty(); @@ -1046,6 +1066,10 @@ export class ZodString extends ZodType { }); } + duration(message?: errorUtil.ErrMessage) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex: RegExp, message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "regex", @@ -1136,12 +1160,17 @@ export class ZodString extends ZodType { return !!this._def.checks.find((ch) => ch.kind === "datetime"); } +<<<<<<< HEAD get isDate() { return !!this._def.checks.find((ch) => ch.kind === "date"); } get isTime() { return !!this._def.checks.find((ch) => ch.kind === "time"); +======= + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); +>>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) } get isEmail() { diff --git a/src/ZodError.ts b/src/ZodError.ts index 61e79316d..c1f7aa3ee 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -101,6 +101,7 @@ export type StringValidation = | "datetime" | "date" | "time" + | "duration" | "ip" | "base64" | { includes: string; position?: number } diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index 44b734583..e3e976b25 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -670,6 +670,19 @@ test("time parsing", () => { // expect(() => time4.parse("00:00:00.000+00:00")).toThrow(); }); +test("duration", () => { + const duration = z.string().duration(); + expect(duration.isDuration).toEqual(true); + + duration.parse("P3Y6M4DT12H30M5S"); + + const result = duration.safeParse("invalidDuration"); + expect(result.success).toEqual(false); + if (!result.success) { + expect(result.error.issues[0].message).toEqual("Invalid duration"); + } +}); + test("IP validation", () => { const ip = z.string().ip(); expect(ip.safeParse("122.122.122.122").success).toBe(true); diff --git a/src/types.ts b/src/types.ts index bcbb343c0..15aa011f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -563,6 +563,7 @@ export type ZodStringCheck = precision: number | null; message?: string; } + | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } | { kind: "base64"; message?: string }; @@ -580,6 +581,9 @@ const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/; const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const durationRegex = + /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/; + // from https://stackoverflow.com/a/46181/1550155 // 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; @@ -906,6 +910,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "ip") { if (!isValidIP(input.data, check.version)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1046,6 +1060,10 @@ export class ZodString extends ZodType { }); } + duration(message?: errorUtil.ErrMessage) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex: RegExp, message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "regex", @@ -1143,6 +1161,9 @@ export class ZodString extends ZodType { get isTime() { return !!this._def.checks.find((ch) => ch.kind === "time"); } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } get isEmail() { return !!this._def.checks.find((ch) => ch.kind === "email"); From c7811f2e2e7992d5e9217a12ba5240df8e992da2 Mon Sep 17 00:00:00 2001 From: "Matt R. Wilson" Date: Sat, 24 Feb 2024 18:15:17 -0700 Subject: [PATCH 2/3] Update Duration to better support ISO 8601-2 Use a better regex to support negatives and decimal fractions to the smallest value. Add a lot of valid and invalid inputs to the test. --- deno/lib/__tests__/string.test.ts | 49 ++++++++++++++++++++++++++++--- deno/lib/types.ts | 2 +- src/__tests__/string.test.ts | 49 ++++++++++++++++++++++++++++--- src/types.ts | 2 +- 4 files changed, 92 insertions(+), 10 deletions(-) diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 0a8a8d081..fee7da743 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -675,11 +675,52 @@ test("duration", () => { const duration = z.string().duration(); expect(duration.isDuration).toEqual(true); - duration.parse("P3Y6M4DT12H30M5S"); + const validDurations = [ + "P3Y6M4DT12H30M5S", + "P2Y9M3DT12H31M8.001S", + "+P3Y6M4DT12H30M5S", + "-PT0.001S", + "+PT0.001S", + "PT0,001S", + "PT12H30M5S", + "-P2M1D", + "P-2M-1D", + "-P5DT10H", + "P-5DT-10H", + "P1Y", + "P2MT30M", + "PT6H", + "P5W", + "P0.5Y", + "P0,5Y", + "P42YT7.004M", + ]; + + const invalidDurations = [ + "foo bar", + "", + " ", + "P", + "T1H", + "P0.5Y1D", + "P0,5Y6M", + "P1YT", + ]; + + for (const val of validDurations) { + const result = duration.safeParse(val); + if (!result.success) { + throw Error(`Valid duration could not be parsed: ${val}`); + } + } + + for (const val of invalidDurations) { + const result = duration.safeParse(val); + + if (result.success) { + throw Error(`Invalid duration was successful parsed: ${val}`); + } - const result = duration.safeParse("invalidDuration"); - expect(result.success).toEqual(false); - if (!result.success) { expect(result.error.issues[0].message).toEqual("Invalid duration"); } >>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 7b65ff00b..4af0d141c 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -588,7 +588,7 @@ const uuidRegex = const nanoidRegex = /^[a-z0-9_-]{21}$/i; ======= const durationRegex = - /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/; + /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; >>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) // from https://stackoverflow.com/a/46181/1550155 diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index e3e976b25..db01a64eb 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -674,11 +674,52 @@ test("duration", () => { const duration = z.string().duration(); expect(duration.isDuration).toEqual(true); - duration.parse("P3Y6M4DT12H30M5S"); + const validDurations = [ + "P3Y6M4DT12H30M5S", + "P2Y9M3DT12H31M8.001S", + "+P3Y6M4DT12H30M5S", + "-PT0.001S", + "+PT0.001S", + "PT0,001S", + "PT12H30M5S", + "-P2M1D", + "P-2M-1D", + "-P5DT10H", + "P-5DT-10H", + "P1Y", + "P2MT30M", + "PT6H", + "P5W", + "P0.5Y", + "P0,5Y", + "P42YT7.004M", + ]; + + const invalidDurations = [ + "foo bar", + "", + " ", + "P", + "T1H", + "P0.5Y1D", + "P0,5Y6M", + "P1YT", + ]; + + for (const val of validDurations) { + const result = duration.safeParse(val); + if (!result.success) { + throw Error(`Valid duration could not be parsed: ${val}`); + } + } + + for (const val of invalidDurations) { + const result = duration.safeParse(val); + + if (result.success) { + throw Error(`Invalid duration was successful parsed: ${val}`); + } - const result = duration.safeParse("invalidDuration"); - expect(result.success).toEqual(false); - if (!result.success) { expect(result.error.issues[0].message).toEqual("Invalid duration"); } }); diff --git a/src/types.ts b/src/types.ts index 15aa011f4..1205276b5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -582,7 +582,7 @@ const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; const nanoidRegex = /^[a-z0-9_-]{21}$/i; const durationRegex = - /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/; + /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; // from https://stackoverflow.com/a/46181/1550155 // old version: too slow, didn't support unicode From 4d81e8c2e82a6498eb12903d5a5c2c33fbd9a101 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Wed, 17 Apr 2024 16:49:20 -0700 Subject: [PATCH 3/3] Fix test --- deno/lib/README.md | 9 ++------- deno/lib/ZodError.ts | 3 --- deno/lib/__tests__/string.test.ts | 5 ++--- deno/lib/benchmarks/datetime.ts | 9 --------- deno/lib/types.ts | 20 ++++++-------------- src/benchmarks/datetime.ts | 9 --------- 6 files changed, 10 insertions(+), 45 deletions(-) diff --git a/deno/lib/README.md b/deno/lib/README.md index 125a56bc1..97cf7a725 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -855,19 +855,14 @@ z.string().regex(regex); z.string().includes(string); z.string().startsWith(string); z.string().endsWith(string); -<<<<<<< HEAD z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed z.string().date(); // ISO date format (YYYY-MM-DD) z.string().time(); // ISO time format (HH:mm:ss[.SSSSSS]) +z.string().duration(); // ISO 8601 duration z.string().ip(); // defaults to allow both IPv4 and IPv6 z.string().base64(); -======= -z.string().datetime(); // ISO 8601; default is without UTC offset, see below for options -z.string().duration(); // ISO 8601 Duration -z.string().ip(); // defaults to IPv4 and IPv6, see below for options ->>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) -// transformations +// transforms z.string().trim(); // trim whitespace z.string().toLowerCase(); // toLowerCase z.string().toUpperCase(); // toUpperCase diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index f587e0e99..e757cd8ba 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -99,12 +99,9 @@ export type StringValidation = | "cuid2" | "ulid" | "datetime" -<<<<<<< HEAD | "date" | "time" -======= | "duration" ->>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) | "ip" | "base64" | { includes: string; position?: number } diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index fee7da743..3f5b95562 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -548,7 +548,6 @@ test("datetime parsing", () => { ).toThrow(); }); -<<<<<<< HEAD test("date", () => { const a = z.string().date(); expect(a.isDate).toEqual(true); @@ -670,7 +669,8 @@ test("time parsing", () => { // expect(() => time4.parse("00:00:00.0")).toThrow(); // expect(() => time4.parse("00:00:00.000")).toThrow(); // expect(() => time4.parse("00:00:00.000+00:00")).toThrow(); -======= +}); + test("duration", () => { const duration = z.string().duration(); expect(duration.isDuration).toEqual(true); @@ -723,7 +723,6 @@ test("duration", () => { expect(result.error.issues[0].message).toEqual("Invalid duration"); } ->>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) }); test("IP validation", () => { diff --git a/deno/lib/benchmarks/datetime.ts b/deno/lib/benchmarks/datetime.ts index 35dab11c1..8de021a5a 100644 --- a/deno/lib/benchmarks/datetime.ts +++ b/deno/lib/benchmarks/datetime.ts @@ -6,15 +6,6 @@ const DATA = "2021-01-01"; const MONTHS_31 = new Set([1, 3, 5, 7, 8, 10, 12]); const MONTHS_30 = new Set([4, 6, 9, 11]); -function generateRandomDatetime(): string { - const year = Math.floor(Math.random() * 3000); - const month = Math.floor(Math.random() * 12) + 1; - const day = Math.floor(Math.random() * 31) + 1; - return `${year}-${month.toString().padStart(2, "0")}-${day - .toString() - .padStart(2, "0")}`; -} - const simpleDatetimeRegex = /^(\d{4})-(\d{2})-(\d{2})$/; const datetimeRegexNoLeapYearValidation = /^\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\d|2\d))$/; diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 4af0d141c..1895e435d 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -553,7 +553,6 @@ export type ZodStringCheck = precision: number | null; message?: string; } -<<<<<<< HEAD | { kind: "date"; // withDate: true; @@ -564,12 +563,9 @@ export type ZodStringCheck = precision: number | null; message?: string; } + | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } | { kind: "base64"; message?: string }; -======= - | { kind: "duration"; message?: string } - | { kind: "ip"; version?: IpVersion; message?: string }; ->>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) export interface ZodStringDef extends ZodTypeDef { checks: ZodStringCheck[]; @@ -584,13 +580,10 @@ const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/; // /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; -<<<<<<< HEAD const nanoidRegex = /^[a-z0-9_-]{21}$/i; -======= const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; ->>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) // from https://stackoverflow.com/a/46181/1550155 // 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; @@ -893,7 +886,6 @@ export class ZodString extends ZodType { }); status.dirty(); } -<<<<<<< HEAD } else if (check.kind === "date") { const regex = dateRegex; @@ -914,14 +906,16 @@ export class ZodString extends ZodType { addIssueToContext(ctx, { code: ZodIssueCode.invalid_string, validation: "time", -======= + message: check.message, + }); + status.dirty(); + } } else if (check.kind === "duration") { if (!durationRegex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); addIssueToContext(ctx, { validation: "duration", code: ZodIssueCode.invalid_string, ->>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) message: check.message, }); status.dirty(); @@ -1160,17 +1154,15 @@ export class ZodString extends ZodType { return !!this._def.checks.find((ch) => ch.kind === "datetime"); } -<<<<<<< HEAD get isDate() { return !!this._def.checks.find((ch) => ch.kind === "date"); } get isTime() { return !!this._def.checks.find((ch) => ch.kind === "time"); -======= + } get isDuration() { return !!this._def.checks.find((ch) => ch.kind === "duration"); ->>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) } get isEmail() { diff --git a/src/benchmarks/datetime.ts b/src/benchmarks/datetime.ts index 35dab11c1..8de021a5a 100644 --- a/src/benchmarks/datetime.ts +++ b/src/benchmarks/datetime.ts @@ -6,15 +6,6 @@ const DATA = "2021-01-01"; const MONTHS_31 = new Set([1, 3, 5, 7, 8, 10, 12]); const MONTHS_30 = new Set([4, 6, 9, 11]); -function generateRandomDatetime(): string { - const year = Math.floor(Math.random() * 3000); - const month = Math.floor(Math.random() * 12) + 1; - const day = Math.floor(Math.random() * 31) + 1; - return `${year}-${month.toString().padStart(2, "0")}-${day - .toString() - .padStart(2, "0")}`; -} - const simpleDatetimeRegex = /^(\d{4})-(\d{2})-(\d{2})$/; const datetimeRegexNoLeapYearValidation = /^\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\d|2\d))$/;