Skip to content

Commit

Permalink
Merge pull request #587 from hymccord/zod
Browse files Browse the repository at this point in the history
Introduce Zod library for verifying JSON
  • Loading branch information
AardWolf authored Sep 25, 2024
2 parents 10d2a46 + d51bb97 commit f64dabf
Show file tree
Hide file tree
Showing 53 changed files with 818 additions and 535 deletions.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,8 @@
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"webpack-merge": "^6.0.1"
},
"dependencies": {
"zod": "^3.23.8"
}
}
26 changes: 24 additions & 2 deletions src/scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {IntakeRejectionEngine} from "./hunt-filter/engine";
import {ConsoleLogger, LogLevel} from './util/logger';
import {getUnixTimestamp} from "./util/time";
import {hgResponseSchema} from "./types/hg";
import {HornHud} from './util/hornHud';
import {parseHgInt} from "./util/number";
import * as successHandlers from './modules/ajax-handlers';
Expand Down Expand Up @@ -327,6 +328,20 @@ import * as detailingFuncs from './modules/details/legacy';
createHunterIdHash();
}

try {
const parsedUrl = new URL(url);
// mobile api calls are not checked
if (parsedUrl.hostname === "www.mousehuntgame.com" && !parsedUrl.pathname.startsWith("/api/")) {
const json = JSON.parse(xhr.responseText);
const parseResult = hgResponseSchema.safeParse(json);
if (!parseResult.success) {
logger.warn("Unexpected response type received", parseResult.error?.message);
}
}
} catch {
// Invalid url, JSON, or response is not JSON
}

for (const handler of ajaxSuccessHandlers) {
if (handler.match(url)) {
handler.execute(xhr.responseJSON);
Expand Down Expand Up @@ -428,12 +443,19 @@ import * as detailingFuncs from './modules/details/legacy';
}

/**
* @param {import("./types/hg").HgResponse} pre_response The object obtained prior to invoking `activeturn.php`.
* @param {import("./types/hg").HgResponse} post_response Parsed JSON representation of the response from calling activeturn.php
* @param {unknown} pre_response The object obtained prior to invoking `activeturn.php`.
* @param {unknown} post_response Parsed JSON representation of the response from calling activeturn.php
*/
function recordHuntWithPrehuntUser(pre_response, post_response) {
logger.debug("In recordHuntWithPrehuntUser pre and post:", pre_response, post_response);

const safeParseResultPre = hgResponseSchema.safeParse(pre_response);
const safeParseResultPost = hgResponseSchema.safeParse(post_response);

if (!safeParseResultPre.success || !safeParseResultPost.success) {
logger.warn("Unexpected response type received", safeParseResultPre.error?.message, safeParseResultPost.error?.message);
}

// General data flow
// - Validate API response object
// - Validate User object
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/modules/stages/environments/valourRift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class ValourRiftStager implements IStager {
readonly environment: string = 'Valour Rift';

addStage(message: IntakeMessage, userPre: User, userPost: User, journal: unknown): void {
const attrs = userPre.environment_atts ?? userPre.enviroment_atts;
const attrs = userPre.enviroment_atts;
const quest = userPre.quests.QuestRiftValour;

if (!quest) {
Expand Down
11 changes: 8 additions & 3 deletions src/scripts/types/hg/environmentAttributes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import * as quests from './quests';
import {z} from "zod";
import {valourRiftEnvironmentAttributesSchema} from "./quests";

export type EnvironmentAttributes = quests.ValourRiftEnvironmentAttributes |
Record<string, never>;
export const environmentAttributesSchema = z.union([
valourRiftEnvironmentAttributesSchema,
z.object({}),
]);

export type EnvironmentAttributes = z.infer<typeof environmentAttributesSchema>;
27 changes: 16 additions & 11 deletions src/scripts/types/hg/hgResponse.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import {InventoryItem} from "./inventoryItem";
import {JournalMarkup} from "./journalMarkup";
import {User} from './user';
import {z} from "zod";
import {userSchema} from "./user";
import {journalMarkupSchema} from "./journalMarkup";
import {inventoryItemSchema} from "./inventoryItem";

export const hgResponseSchema = z.object({
user: userSchema,
page: z.unknown().optional(),
success: z.union([z.literal(0), z.literal(1)]),
active_turn: z.boolean().optional(),
journal_markup: z.array(journalMarkupSchema).optional(),
inventory: z.union([
z.record(z.string(), inventoryItemSchema),
z.array(z.unknown()),
]).optional(),
});

export interface HgResponse {
user: User;
page?: unknown;
success: 0 | 1;
active_turn?: boolean;
journal_markup?: JournalMarkup[];
inventory?: Record<string, InventoryItem> | [];
}
export type HgResponse = z.infer<typeof hgResponseSchema>;
16 changes: 9 additions & 7 deletions src/scripts/types/hg/inventoryItem.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import {z} from "zod";


export interface InventoryItem {
export const inventoryItemSchema = z.object({
/** HitGrab's internal number id of item */
item_id: number;
item_id: z.coerce.number(),
/** Friendly display name of item */
name: string;
name: z.string(),
/** Unique snake_case identifying name of item */
type: string;
type: z.string(),
/** Item category: bait, crafting, stat, etc... */
// classification: Classification;
/** Total amount of item in user inventory */
quantity: number;
}
quantity: z.coerce.number(),
});

export type InventoryItem = z.infer<typeof inventoryItemSchema>;
13 changes: 9 additions & 4 deletions src/scripts/types/hg/journalMarkup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {RenderData} from "./renderData";
import {z} from "zod";
import {renderDataSchema} from "./renderData";

export interface JournalMarkup {
render_data: RenderData;
}
export const journalMarkupSchema = z.object({
render_data: renderDataSchema,
//publish_data: PublishData;
//wall_actions: WallActions;
});

export type JournalMarkup = z.infer<typeof journalMarkupSchema>;
18 changes: 11 additions & 7 deletions src/scripts/types/hg/quests/balacksCove.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
export interface QuestBalacksCove {
tide: {
level: 'low' | 'med' | 'high'
direction: 'in' | 'out'
percent: number
}
}
import {z} from "zod";

export const questBalacksCoveSchema = z.object({
tide: z.object({
level: z.union([z.literal('low'), z.literal('med'), z.literal('high')]),
direction: z.union([z.literal('in'), z.literal('out')]),
percent: z.coerce.number(),
}),
});

export type QuestBalacksCove = z.infer<typeof questBalacksCoveSchema>;
80 changes: 46 additions & 34 deletions src/scripts/types/hg/quests/bountifulBeanstalk.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,49 @@
export type QuestBountifulBeanstalk =
| BeanstalkAttributes
| CastleAttributes;
import {z} from "zod";

export interface BeanstalkAttributes {
in_castle: false;
beanstalk: {
is_boss_encounter: boolean;
};
}
export const beanstalkAttributesSchema = z.object({
in_castle: z.literal(false),
beanstalk: z.object({
is_boss_encounter: z.boolean(),
}),
});

export interface CastleAttributes {
in_castle: true;
castle: {
is_boss_chase: boolean;
is_boss_encounter: boolean;
current_floor: {
type: string;
name: string;
};
current_room: {
type: string;
name: string;
},
next_room: {
type: string;
name: string;
},
room_position: number;
};
embellishments: Embellishment[];
}
export const embellishmentSchema = z.object({
type: z.union([
z.literal("golden_key"),
z.literal("golden_feather"),
z.literal("ruby_remover"),
]),
is_active: z.boolean(),
});

export interface Embellishment {
type: 'golden_key' | 'golden_feather' | 'ruby_remover';
is_active: boolean;
}
export const castleAttributesSchema = z.object({
in_castle: z.literal(true),
castle: z.object({
is_boss_chase: z.boolean(),
is_boss_encounter: z.boolean(),
current_floor: z.object({
type: z.string(),
name: z.string(),
}),
current_room: z.object({
type: z.string(),
name: z.string(),
}),
next_room: z.object({
type: z.string(),
name: z.string(),
}),
room_position: z.coerce.number(),
}),
embellishments: z.array(embellishmentSchema),
});

export const questBountifulBeanstalkSchema = z.union([
beanstalkAttributesSchema,
castleAttributesSchema,
]);

export type BeanstalkAttributes = z.infer<typeof beanstalkAttributesSchema>;
export type CastleAttributes = z.infer<typeof castleAttributesSchema>;
export type Embellishment = z.infer<typeof embellishmentSchema>;
export type QuestBountifulBeanstalk = z.infer<typeof questBountifulBeanstalkSchema>;
10 changes: 7 additions & 3 deletions src/scripts/types/hg/quests/bristleWoodsRift.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export interface QuestRiftBristleWoods {
chamber_name: string
}
import {z} from "zod";

export const questRiftBristleWoodsSchema = z.object({
chamber_name: z.string(),
});

export type QuestRiftBristleWoods = z.infer<typeof questRiftBristleWoodsSchema>;
10 changes: 7 additions & 3 deletions src/scripts/types/hg/quests/burroughsRift.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
export interface QuestRiftBurroughs {
mist_tier: string // MistTier
}
import {z} from "zod";

export const MistTiers = ['tier_0', 'tier_1', 'tier_2', 'tier_3'] as const;
const mistTierSchema = z.enum(MistTiers);

export const questRiftBurroughsSchema = z.object({
mist_tier: mistTierSchema,
});

export type MistTier = typeof MistTiers[number];
export type QuestRiftBurroughs = z.infer<typeof questRiftBurroughsSchema>;
12 changes: 8 additions & 4 deletions src/scripts/types/hg/quests/clawShotCity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export interface QuestClawShotCity {
map_active: boolean;
has_wanted_poster: boolean;
}
import {z} from "zod";

export const questClawShotCitySchema = z.object({
map_active: z.boolean(),
has_wanted_poster: z.boolean(),
});

export type QuestClawShotCity = z.infer<typeof questClawShotCitySchema>;
63 changes: 35 additions & 28 deletions src/scripts/types/hg/quests/draconicDepths.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
export type QuestDraconicDepths = CrucibleEnvironment | CavernEnvironment;
import {z} from 'zod';

export const CavernTypes = [
"flame_cavern",
"double_flame_cavern",
"triple_flame_cavern",
"ice_lair",
"double_ice_lair",
"triple_ice_lair",
"toxic_tunnels",
"double_toxic_tunnels",
"triple_toxic_tunnels",
"elemental_dragon_den",
'flame_cavern',
'double_flame_cavern',
'triple_flame_cavern',
'ice_lair',
'double_ice_lair',
'triple_ice_lair',
'toxic_tunnels',
'double_toxic_tunnels',
'triple_toxic_tunnels',
'elemental_dragon_den',
] as const;
export type CavernType = (typeof CavernTypes)[number];
const cavernTypeSchema = z.enum(CavernTypes);
export type CavernType = z.infer<typeof cavernTypeSchema>;

interface CrucibleEnvironment {
in_cavern: false;
}
const crucibleEnvironmentSchema = z.object({
in_cavern: z.literal(false),
});

interface CavernEnvironment {
in_cavern: true;
cavern: {
type: CavernType;
category: "fire" | "ice" | "poison" | "elemental";
loot_tier: {
current_tier: number;
tier_data: {
threshold: number;
}[]
};
};
}
const cavernEnvironmentSchema = z.object({
in_cavern: z.literal(true),
cavern: z.object({
type: cavernTypeSchema,
category: z.enum(['fire', 'ice', 'poison', 'elemental']),
loot_tier: z.object({
current_tier: z.number(),
tier_data: z.array(z.object({
threshold: z.number(),
})),
}),
}),
});

export const questDraconicDepthsSchema = z.discriminatedUnion('in_cavern', [
crucibleEnvironmentSchema,
cavernEnvironmentSchema,
]);
export type QuestDraconicDepths = z.infer<typeof questDraconicDepthsSchema>;
Loading

0 comments on commit f64dabf

Please sign in to comment.