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

Optimisations for zod 3.23 #1689

Merged
merged 11 commits into from
Apr 22, 2024
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Version 18

### v18.1.0

- Optimization for `zod` 3.23:
- `zod` 3.23 offers
[several features on handling strings](https://github.com/colinhacks/zod/releases/tag/v3.23.0);
- It's also claimed to be "the final 3.x release before Zod 4.0".;
- Using the featured `zod` refinements in the following proprietary schemas: `ez.dateIn()` and `ez.file("base64")`;
- The changes are non-breaking and the compatibility to `zod` 3.22 remains;
- Validation error messages will depend on actual `zod` version installed.

### v18.0.0

- **Breaking changes**:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
"tsx": "^4.6.2",
"typescript": "^5.2.2",
"vitest": "^1.5.0",
"zod": "^3.22.3"
"zod": "^3.23.0"
},
"keywords": [
"nodejs",
Expand Down
17 changes: 10 additions & 7 deletions src/date-in-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { isValidDate, isoDateRegex } from "./schema-helpers";

export const ezDateInKind = "DateIn";

export const dateIn = () =>
proprietary(
export const dateIn = () => {
const base = z.string();
const hasDateMethod = base.date?.() instanceof z.ZodString;
const schema = hasDateMethod
? z.union([base.date(), base.datetime(), base.datetime({ local: true })])
: base.regex(isoDateRegex); // @todo remove after min zod v3.23 (v19)

return proprietary(
ezDateInKind,
z
.string()
.regex(isoDateRegex)
.transform((str) => new Date(str))
.pipe(z.date().refine(isValidDate)),
schema.transform((str) => new Date(str)).pipe(z.date().refine(isValidDate)),
);
};
5 changes: 4 additions & 1 deletion src/documentation-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ export const depictFile: Depicter<z.ZodType> = ({ schema }) => ({
type: "string",
format:
schema instanceof z.ZodString
? schema._def.checks.find((check) => check.kind === "regex")
? schema._def.checks.find(
/** @todo remove regex check when min zod v3.23 (v19) */
(check) => check.kind === "regex" || check.kind === "base64",
)
? "byte"
: "file"
: "binary",
Expand Down
14 changes: 10 additions & 4 deletions src/file-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@ const bufferSchema = z.custom<Buffer>((subject) => Buffer.isBuffer(subject), {
message: "Expected Buffer",
});

/** @todo remove after min zod v3.23 (v19) */
const base64Regex =
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;

const variants = {
buffer: () => proprietary(ezFileKind, bufferSchema),
string: () => proprietary(ezFileKind, z.string()),
binary: () => proprietary(ezFileKind, bufferSchema.or(z.string())),
base64: () =>
proprietary(
base64: () => {
const base = z.string();
const hasBase64Method = base.base64?.() instanceof z.ZodString;
return proprietary(
ezFileKind,
z.string().regex(base64Regex, "Does not match base64 encoding"),
),
hasBase64Method
? base.base64()
: base.regex(base64Regex, "Does not match base64 encoding"), // @todo remove after min zod v3.23 (v19)
);
},
};

type Variants = typeof variants;
Expand Down
1 change: 1 addition & 0 deletions src/schema-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const isValidDate = (date: Date): boolean => !isNaN(date.getTime());
* @example 2021-01-01T00:00:00Z
* @example 2021-01-01T00:00:00
* @example 2021-01-01
* @todo remove after min zod v3.23 (v19)
*/
export const isoDateRegex =
/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?)?Z?$/;
95 changes: 95 additions & 0 deletions tests/unit/__snapshots__/date-in-schema.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`ez.dateIn() current mode > parsing > should handle invalid date 1`] = `
[
{
"code": "invalid_string",
"message": "Invalid date",
"path": [],
"validation": "date",
},
]
`;

exports[`ez.dateIn() current mode > parsing > should handle invalid format 1`] = `
[
{
"code": "invalid_string",
"message": "Invalid date",
"path": [],
"validation": "date",
},
]
`;

exports[`ez.dateIn() current mode > parsing > should handle wrong parsed type 1`] = `
[
{
"code": "invalid_union",
"message": "Invalid input",
"path": [],
"unionErrors": [
[ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": [],
"message": "Expected string, received number"
}
]],
[ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": [],
"message": "Expected string, received number"
}
]],
[ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": [],
"message": "Expected string, received number"
}
]],
],
},
]
`;

exports[`ez.dateIn() legacy mode > parsing > should handle invalid date 1`] = `
[
{
"code": "invalid_date",
"message": "Invalid date",
"path": [],
},
]
`;

exports[`ez.dateIn() legacy mode > parsing > should handle invalid format 1`] = `
[
{
"code": "invalid_string",
"message": "Invalid",
"path": [],
"validation": "regex",
},
]
`;

exports[`ez.dateIn() legacy mode > parsing > should handle wrong parsed type 1`] = `
[
{
"code": "invalid_type",
"expected": "string",
"message": "Expected string, received number",
"path": [],
"received": "number",
},
]
`;
23 changes: 23 additions & 0 deletions tests/unit/__snapshots__/file-schema.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`ez.file() current mode > parsing > should perform additional check for base64 file 1`] = `
[
{
"code": "invalid_string",
"message": "Invalid base64",
"path": [],
"validation": "base64",
},
]
`;

exports[`ez.file() legacy mode > parsing > should perform additional check for base64 file 1`] = `
[
{
"code": "invalid_string",
"message": "Does not match base64 encoding",
"path": [],
"validation": "regex",
},
]
`;
40 changes: 14 additions & 26 deletions tests/unit/date-in-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { z } from "zod";
import { getMeta } from "../../src/metadata";
import { ez } from "../../src";
import { describe, expect, test } from "vitest";
import { beforeAll, describe, expect, test, vi } from "vitest";

describe.each(["current", "legacy"])("ez.dateIn() %s mode", (mode) => {
// @todo remove after min zod v3.23 (v19)
beforeAll(() => {
if (mode === "legacy") {
vi.spyOn(z.ZodString.prototype, "date").mockImplementation(
() => null as unknown as z.ZodString,
);
}
});

describe("ez.dateIn()", () => {
describe("creation", () => {
test("should create an instance", () => {
const schema = ez.dateIn();
Expand All @@ -18,15 +27,7 @@ describe("ez.dateIn()", () => {
const result = schema.safeParse(123);
expect(result.success).toBeFalsy();
if (!result.success) {
expect(result.error.issues).toEqual([
{
code: "invalid_type",
expected: "string",
message: "Expected string, received number",
path: [],
received: "number",
},
]);
expect(result.error.issues).toMatchSnapshot();
}
});

Expand All @@ -50,13 +51,7 @@ describe("ez.dateIn()", () => {
const result = schema.safeParse("2022-01-32");
expect(result.success).toBeFalsy();
if (!result.success) {
expect(result.error.issues).toEqual([
{
code: "invalid_date",
message: "Invalid date",
path: [],
},
]);
expect(result.error.issues).toMatchSnapshot();
}
});

Expand All @@ -65,14 +60,7 @@ describe("ez.dateIn()", () => {
const result = schema.safeParse("12.01.2021");
expect(result.success).toBeFalsy();
if (!result.success) {
expect(result.error.issues).toEqual([
{
code: "invalid_string",
message: "Invalid",
validation: "regex",
path: [],
},
]);
expect(result.error.issues).toMatchSnapshot();
}
});
});
Expand Down
22 changes: 12 additions & 10 deletions tests/unit/file-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@ import { z } from "zod";
import { getMeta } from "../../src/metadata";
import { ez } from "../../src";
import { readFile } from "node:fs/promises";
import { describe, expect, test } from "vitest";
import { beforeAll, describe, expect, test, vi } from "vitest";

describe.each(["current", "legacy"])("ez.file() %s mode", (mode) => {
// @todo remove after min zod v3.23 (v19)
beforeAll(() => {
if (mode === "legacy") {
vi.spyOn(z.ZodString.prototype, "base64").mockImplementation(
() => null as unknown as z.ZodString,
);
}
});

describe("ez.file()", () => {
describe("creation", () => {
test("should create an instance being string by default", () => {
const schema = ez.file();
Expand Down Expand Up @@ -76,14 +85,7 @@ describe("ez.file()", () => {
const result = schema.safeParse("~~~~");
expect(result.success).toBeFalsy();
if (!result.success) {
expect(result.error.issues).toEqual([
{
code: "invalid_string",
message: "Does not match base64 encoding",
validation: "regex",
path: [],
},
]);
expect(result.error.issues).toMatchSnapshot();
}
});

Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4764,7 +4764,7 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==

zod@^3.22.3:
zod@^3.23.0:
version "3.23.0"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.0.tgz#a25dab5052702834233e0041e9dd8b6a8480e744"
integrity sha512-OFLT+LTocvabn6q76BTwVB0hExEBS0IduTr3cqZyMqEDbOnYmcU+y0tUAYbND4uwclpBGi4I4UUBGzylWpjLGA==
Loading