Skip to content

Commit 353fc14

Browse files
authored
refactor: move parseJsonWithSchema to utils package (@fehmer) (monkeytypegame#6109)
1 parent 4baae8f commit 353fc14

File tree

7 files changed

+96
-26
lines changed

7 files changed

+96
-26
lines changed

frontend/src/ts/utils/misc.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
Mode2,
88
PersonalBests,
99
} from "@monkeytype/contracts/schemas/shared";
10-
import { ZodError, ZodSchema } from "zod";
1110
import {
1211
CustomTextDataWithTextLen,
1312
Result,
@@ -655,26 +654,6 @@ export function isObject(obj: unknown): obj is Record<string, unknown> {
655654
return typeof obj === "object" && !Array.isArray(obj) && obj !== null;
656655
}
657656

658-
/**
659-
* Parse a JSON string into an object and validate it against a schema
660-
* @param input JSON string
661-
* @param schema Zod schema to validate the JSON against
662-
* @returns The parsed JSON object
663-
*/
664-
export function parseJsonWithSchema<T>(input: string, schema: ZodSchema<T>): T {
665-
try {
666-
const jsonParsed = JSON.parse(input) as unknown;
667-
const zodParsed = schema.parse(jsonParsed);
668-
return zodParsed;
669-
} catch (error) {
670-
if (error instanceof ZodError) {
671-
throw new Error(error.errors.map((err) => err.message).join("\n"));
672-
} else {
673-
throw error;
674-
}
675-
}
676-
}
677-
678657
export function deepClone<T>(obj: T[]): T[];
679658
export function deepClone<T extends object>(obj: T): T;
680659
export function deepClone<T>(obj: T): T;

frontend/src/ts/utils/url-handler.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
Difficulty,
2323
} from "@monkeytype/contracts/schemas/configs";
2424
import { z } from "zod";
25+
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
2526

2627
export async function linkDiscord(hashOverride: string): Promise<void> {
2728
if (!hashOverride) return;
@@ -78,10 +79,7 @@ export function loadCustomThemeFromUrl(getOverride?: string): void {
7879

7980
let decoded: z.infer<typeof customThemeUrlDataSchema>;
8081
try {
81-
decoded = Misc.parseJsonWithSchema(
82-
atob(getValue),
83-
customThemeUrlDataSchema
84-
);
82+
decoded = parseJsonWithSchema(atob(getValue), customThemeUrlDataSchema);
8583
} catch (e) {
8684
console.log("Custom theme URL decoding failed", e);
8785
Notifications.add(
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as Util from "../src/util";
2+
3+
describe("util", () => {
4+
describe("stringToFunboxNames", () => {
5+
it("should get single funbox", () => {
6+
expect(Util.stringToFunboxNames("58008")).toEqual(["58008"]);
7+
});
8+
it("should fail for unknown funbox name", () => {
9+
expect(() => Util.stringToFunboxNames("unknown")).toThrowError(
10+
new Error("Invalid funbox name: unknown")
11+
);
12+
});
13+
it("should split multiple funboxes by hash", () => {
14+
expect(Util.stringToFunboxNames("58008#choo_choo")).toEqual([
15+
"58008",
16+
"choo_choo",
17+
]);
18+
});
19+
});
20+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { parseWithSchema } from "../src/json";
2+
import { z } from "zod";
3+
4+
describe("json", () => {
5+
describe("parseWithSchema", () => {
6+
const schema = z.object({
7+
test: z.boolean().optional(),
8+
name: z.string(),
9+
nested: z.object({ foo: z.string() }).strict().optional(),
10+
});
11+
it("should parse", () => {
12+
const json = `{
13+
"test":true,
14+
"name":"bob",
15+
"unknown":"unknown",
16+
"nested":{
17+
"foo":"bar"
18+
}
19+
}`;
20+
21+
expect(parseWithSchema(json, schema)).toStrictEqual({
22+
test: true,
23+
name: "bob",
24+
nested: { foo: "bar" },
25+
});
26+
});
27+
it("should fail with invalid schema", () => {
28+
const json = `{
29+
"test":"yes",
30+
"nested":{
31+
"foo":1
32+
}
33+
}`;
34+
35+
expect(() => parseWithSchema(json, schema)).toThrowError(
36+
new Error(
37+
`"test" Expected boolean, received string\n"name" Required\n"nested.foo" Expected string, received number`
38+
)
39+
);
40+
});
41+
});
42+
});

packages/util/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"madge": "8.0.0",
1919
"rimraf": "6.0.1",
2020
"typescript": "5.5.4",
21-
"vitest": "2.0.5"
21+
"vitest": "2.0.5",
22+
"zod": "3.23.8"
2223
},
2324
"exports": {
2425
".": {

packages/util/src/json.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ZodError, ZodIssue, ZodSchema } from "zod";
2+
3+
/**
4+
* Parse a JSON string into an object and validate it against a schema
5+
* @param json JSON string
6+
* @param schema Zod schema to validate the JSON against
7+
* @returns The parsed JSON object
8+
*/
9+
export function parseWithSchema<T>(json: string, schema: ZodSchema<T>): T {
10+
try {
11+
const jsonParsed = JSON.parse(json) as unknown;
12+
const zodParsed = schema.parse(jsonParsed);
13+
return zodParsed;
14+
} catch (error) {
15+
if (error instanceof ZodError) {
16+
throw new Error(error.issues.map(prettyErrorMessage).join("\n"));
17+
} else {
18+
throw error;
19+
}
20+
}
21+
}
22+
23+
function prettyErrorMessage(issue: ZodIssue | undefined): string {
24+
if (issue === undefined) return "";
25+
const path = issue.path.length > 0 ? `"${issue.path.join(".")}" ` : "";
26+
return `${path}${issue.message}`;
27+
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)