mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2026-01-08 08:25:05 +08:00
refactor: replace JSON.parse with parseJsonWithSchema (@dev-mohit06) (#6207)
## Description
Replaces raw JSON parsing with schema-based validation across frontend
TypeScript files to improve type safety and error handling.
### Scope of Changes
- Updated JSON parsing in:
- `account.ts`
- `import-export-settings.ts`
- `analytics-controller.ts`
- `local-storage-with-schema.ts`
- `url-handler.ts`
- `commandline/lists.ts`
- `test/wikipedia.ts`
- Added schema in `test/custom-text.ts`:
```typescript
export const customTextDataSchema = z.object({
text: z.array(z.string()),
mode: CustomTextModeSchema,
limit: z.object({
value: z.number(),
mode: CustomTextLimitModeSchema
}),
pipeDelimiter: z.boolean(),
});
```
### Benefits
- Enhanced runtime type safety
- More robust error handling
- Consistent JSON parsing approach
### Checks
- [x] Follows Conventional Commits
- [x] Includes GitHub username
- [ ] Adding quotes? (N/A)
- [ ] Adding language/theme? (N/A)
---------
Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
86cb17be82
commit
3510ea9760
8 changed files with 92 additions and 35 deletions
|
|
@ -108,6 +108,7 @@ import * as FPSCounter from "../elements/fps-counter";
|
|||
import { migrateConfig } from "../utils/config";
|
||||
import { PartialConfigSchema } from "@monkeytype/contracts/schemas/configs";
|
||||
import { Command, CommandsSubgroup } from "./types";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
|
||||
const layoutsPromise = JSONData.getLayoutsList();
|
||||
layoutsPromise
|
||||
|
|
@ -362,8 +363,9 @@ export const commands: CommandsSubgroup = {
|
|||
exec: async ({ input }): Promise<void> => {
|
||||
if (input === undefined || input === "") return;
|
||||
try {
|
||||
const parsedConfig = PartialConfigSchema.strip().parse(
|
||||
JSON.parse(input)
|
||||
const parsedConfig = parseJsonWithSchema(
|
||||
input,
|
||||
PartialConfigSchema.strip()
|
||||
);
|
||||
await UpdateConfig.apply(migrateConfig(parsedConfig));
|
||||
UpdateConfig.saveFullConfigToLocalStorage();
|
||||
|
|
|
|||
|
|
@ -6,13 +6,17 @@ import {
|
|||
} from "firebase/analytics";
|
||||
import { app as firebaseApp } from "../firebase";
|
||||
import { createErrorMessage } from "../utils/misc";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
import { z } from "zod";
|
||||
|
||||
let analytics: AnalyticsType;
|
||||
|
||||
type AcceptedCookies = {
|
||||
security: boolean;
|
||||
analytics: boolean;
|
||||
};
|
||||
const AcceptedCookiesSchema = z.object({
|
||||
security: z.boolean(),
|
||||
analytics: z.boolean(),
|
||||
});
|
||||
|
||||
type AcceptedCookies = z.infer<typeof AcceptedCookiesSchema>;
|
||||
|
||||
export async function log(
|
||||
eventName: string,
|
||||
|
|
@ -26,9 +30,14 @@ export async function log(
|
|||
}
|
||||
|
||||
const lsString = localStorage.getItem("acceptedCookies");
|
||||
let acceptedCookies;
|
||||
let acceptedCookies: AcceptedCookies | null;
|
||||
if (lsString !== undefined && lsString !== null && lsString !== "") {
|
||||
acceptedCookies = JSON.parse(lsString) as AcceptedCookies;
|
||||
try {
|
||||
acceptedCookies = parseJsonWithSchema(lsString, AcceptedCookiesSchema);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse accepted cookies:", e);
|
||||
acceptedCookies = null;
|
||||
}
|
||||
} else {
|
||||
acceptedCookies = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { isAuthenticated } from "../firebase";
|
|||
import * as Notifications from "../elements/notifications";
|
||||
import * as EditResultTagsModal from "../modals/edit-result-tags";
|
||||
import * as AddFilterPresetModal from "../modals/new-filter-preset";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
import { z } from "zod";
|
||||
|
||||
const accountPage = document.querySelector("#pageAccount") as HTMLElement;
|
||||
|
||||
|
|
@ -36,12 +38,15 @@ $(accountPage).on("click", ".editProfileButton", () => {
|
|||
EditProfileModal.show();
|
||||
});
|
||||
|
||||
const TagsArraySchema = z.array(z.string());
|
||||
|
||||
$(accountPage).on("click", ".group.history .resultEditTagsButton", (e) => {
|
||||
const resultid = $(e.target).attr("data-result-id");
|
||||
const tags = $(e.target).attr("data-tags");
|
||||
|
||||
EditResultTagsModal.show(
|
||||
resultid ?? "",
|
||||
JSON.parse(tags ?? "[]") as string[],
|
||||
parseJsonWithSchema(tags ?? "[]", TagsArraySchema),
|
||||
"accountPage"
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import * as UpdateConfig from "../config";
|
|||
import * as Notifications from "../elements/notifications";
|
||||
import AnimatedModal from "../utils/animated-modal";
|
||||
import { migrateConfig } from "../utils/config";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
|
||||
type State = {
|
||||
mode: "import" | "export";
|
||||
|
|
@ -47,8 +48,9 @@ const modal = new AnimatedModal({
|
|||
return;
|
||||
}
|
||||
try {
|
||||
const parsedConfig = PartialConfigSchema.strip().parse(
|
||||
JSON.parse(state.value)
|
||||
const parsedConfig = parseJsonWithSchema(
|
||||
state.value,
|
||||
PartialConfigSchema.strip()
|
||||
);
|
||||
await UpdateConfig.apply(migrateConfig(parsedConfig));
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const customTextLongLS = new LocalStorageWithSchema({
|
|||
fallback: {},
|
||||
});
|
||||
|
||||
const CustomTextSettingsSchema = z.object({
|
||||
export const CustomTextSettingsSchema = z.object({
|
||||
text: z.array(z.string()),
|
||||
mode: CustomTextModeSchema,
|
||||
limit: z.object({ value: z.number(), mode: CustomTextLimitModeSchema }),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import * as Loader from "../elements/loader";
|
|||
import * as Misc from "../utils/misc";
|
||||
import * as Strings from "../utils/strings";
|
||||
import * as JSONData from "../utils/json-data";
|
||||
import { z } from "zod";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
|
||||
export async function getTLD(
|
||||
languageGroup: JSONData.LanguageGroup
|
||||
|
|
@ -241,6 +243,18 @@ type SectionObject = {
|
|||
author: string;
|
||||
};
|
||||
|
||||
// Section Schema
|
||||
const SectionSchema = z.object({
|
||||
query: z.object({
|
||||
pages: z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
extract: z.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export async function getSection(language: string): Promise<JSONData.Section> {
|
||||
// console.log("Getting section");
|
||||
Loader.show();
|
||||
|
|
@ -285,10 +299,17 @@ export async function getSection(language: string): Promise<JSONData.Section> {
|
|||
sectionReq.onload = (): void => {
|
||||
if (sectionReq.readyState === 4) {
|
||||
if (sectionReq.status === 200) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
let sectionText = JSON.parse(sectionReq.responseText).query.pages[
|
||||
pageid.toString()
|
||||
].extract as string;
|
||||
const parsedResponse = parseJsonWithSchema(
|
||||
sectionReq.responseText,
|
||||
SectionSchema
|
||||
);
|
||||
const page = parsedResponse.query.pages[pageid.toString()];
|
||||
if (!page) {
|
||||
Loader.hide();
|
||||
rej("Page not found");
|
||||
return;
|
||||
}
|
||||
let sectionText = page.extract;
|
||||
|
||||
// Converting to one paragraph
|
||||
sectionText = sectionText.replace(/<\/p><p>+/g, " ");
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import * as Loader from "../elements/loader";
|
|||
import * as AccountButton from "../elements/account-button";
|
||||
import { restart as restartTest } from "../test/test-logic";
|
||||
import * as ChallengeController from "../controllers/challenge-controller";
|
||||
import { Mode, Mode2 } from "@monkeytype/contracts/schemas/shared";
|
||||
import {
|
||||
DifficultySchema,
|
||||
Mode2Schema,
|
||||
ModeSchema,
|
||||
} from "@monkeytype/contracts/schemas/shared";
|
||||
import {
|
||||
CustomBackgroundFilter,
|
||||
CustomBackgroundFilterSchema,
|
||||
|
|
@ -19,7 +23,6 @@ import {
|
|||
CustomBackgroundSizeSchema,
|
||||
CustomThemeColors,
|
||||
CustomThemeColorsSchema,
|
||||
Difficulty,
|
||||
} from "@monkeytype/contracts/schemas/configs";
|
||||
import { z } from "zod";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
|
|
@ -129,24 +132,35 @@ export function loadCustomThemeFromUrl(getOverride?: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
type SharedTestSettings = [
|
||||
Mode | null,
|
||||
Mode2<Mode> | null,
|
||||
CustomText.CustomTextData | null,
|
||||
boolean | null,
|
||||
boolean | null,
|
||||
string | null,
|
||||
Difficulty | null,
|
||||
string | null
|
||||
];
|
||||
const TestSettingsSchema = z.tuple([
|
||||
ModeSchema.nullable(),
|
||||
Mode2Schema.nullable(),
|
||||
CustomText.CustomTextSettingsSchema.nullable(),
|
||||
z.boolean().nullable(), //punctuation
|
||||
z.boolean().nullable(), //numbers
|
||||
z.string().nullable(), //language
|
||||
DifficultySchema.nullable(),
|
||||
z.string().nullable(), //funbox
|
||||
]);
|
||||
|
||||
type SharedTestSettings = z.infer<typeof TestSettingsSchema>;
|
||||
|
||||
export function loadTestSettingsFromUrl(getOverride?: string): void {
|
||||
const getValue = Misc.findGetParameter("testSettings", getOverride);
|
||||
if (getValue === null) return;
|
||||
|
||||
const de = JSON.parse(
|
||||
decompressFromURI(getValue) ?? ""
|
||||
) as SharedTestSettings;
|
||||
let de: SharedTestSettings;
|
||||
try {
|
||||
const decompressed = decompressFromURI(getValue) ?? "";
|
||||
de = parseJsonWithSchema(decompressed, TestSettingsSchema);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse test settings:", e);
|
||||
Notifications.add(
|
||||
"Failed to load test settings from URL: " + (e as Error).message,
|
||||
0
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const applied: Record<string, string> = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ZodError, ZodIssue, ZodSchema } from "zod";
|
||||
import { z, ZodError, ZodIssue } from "zod";
|
||||
|
||||
/**
|
||||
* Parse a JSON string into an object and validate it against a schema
|
||||
|
|
@ -6,11 +6,15 @@ import { ZodError, ZodIssue, ZodSchema } from "zod";
|
|||
* @param schema Zod schema to validate the JSON against
|
||||
* @returns The parsed JSON object
|
||||
*/
|
||||
export function parseWithSchema<T>(json: string, schema: ZodSchema<T>): T {
|
||||
export function parseWithSchema<T extends z.ZodTypeAny>(
|
||||
json: string,
|
||||
schema: T
|
||||
): z.infer<T> {
|
||||
try {
|
||||
const jsonParsed = JSON.parse(json) as unknown;
|
||||
const zodParsed = schema.parse(jsonParsed);
|
||||
return zodParsed;
|
||||
// hits is fine to ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return schema.parse(jsonParsed) as z.infer<T>;
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
throw new Error(error.issues.map(prettyErrorMessage).join("\n"));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue