Merge pull request #85 from seditionist/main

Add Custom Errors
This commit is contained in:
Max Swartwout
2023-11-26 19:52:07 -05:00
committed by GitHub
5 changed files with 130 additions and 96 deletions

View File

@@ -12,7 +12,7 @@ import {
getSkillPageURL, getSkillPageURL,
getStatsURL, getStatsURL,
BOSSES, BOSSES,
INVALID_FORMAT_ERROR, InvalidFormatError,
BH_MODES, BH_MODES,
parseJsonStats, parseJsonStats,
HiscoresResponse HiscoresResponse
@@ -276,12 +276,12 @@ test('Parse CSV with unknown activity', () => {
const statsWithUnknownActivity = `${lynxTitanStats} const statsWithUnknownActivity = `${lynxTitanStats}
-1,-1`; -1,-1`;
expect(() => parseStats(statsWithUnknownActivity)).toThrow( expect(() => parseStats(statsWithUnknownActivity)).toThrow(
INVALID_FORMAT_ERROR InvalidFormatError
); );
}); });
test('Parse invalid CSV', () => { test('Parse invalid CSV', () => {
expect(() => parseStats('invalid')).toThrow(INVALID_FORMAT_ERROR); expect(() => parseStats('invalid')).toThrow(InvalidFormatError);
}); });
describe('Get name format', () => { describe('Get name format', () => {
@@ -580,6 +580,12 @@ describe('Get stats options', () => {
) )
).toBeFalsy(); ).toBeFalsy();
}); });
it('omits excluded gamemodes', async () => {
const response = await getStats(rsn, {
otherGamemodes: ['ironman', 'ultimate']
});
expect(response.hardcore).toBeUndefined();
});
}); });
test('CSV and JSON parsing outputs identical object', async () => { test('CSV and JSON parsing outputs identical object', async () => {

View File

@@ -67,7 +67,10 @@
], ],
"ignorePatterns": [ "ignorePatterns": [
"**/@types/*" "**/@types/*"
] ],
"rules": {
"max-classes-per-file": "off"
}
}, },
"prettier": { "prettier": {
"trailingComma": "none", "trailingComma": "none",

View File

@@ -1,8 +1,4 @@
import { import axios, { AxiosRequestConfig } from 'axios';
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig
} from 'axios';
import { BinaryData, JSDOM } from 'jsdom'; import { BinaryData, JSDOM } from 'jsdom';
import { import {
Player, Player,
@@ -35,9 +31,10 @@ import {
getActivityPageURL, getActivityPageURL,
httpGet, httpGet,
BOSSES, BOSSES,
INVALID_FORMAT_ERROR, InvalidFormatError,
PlayerNotFoundError,
HiScoresError,
validateRSN, validateRSN,
PLAYER_NOT_FOUND_ERROR,
FORMATTED_SKILL_NAMES, FORMATTED_SKILL_NAMES,
FORMATTED_BH_NAMES, FORMATTED_BH_NAMES,
FORMATTED_CLUE_NAMES, FORMATTED_CLUE_NAMES,
@@ -68,8 +65,12 @@ export async function getOfficialStats(
try { try {
const response = await httpGet<HiscoresResponse>(url, config); const response = await httpGet<HiscoresResponse>(url, config);
return response.data; return response.data;
} catch { } catch (err) {
throw Error(PLAYER_NOT_FOUND_ERROR); if (!axios.isAxiosError(err)) throw err;
if (err.response?.status === 404) throw new PlayerNotFoundError();
throw new HiScoresError();
} }
} }
@@ -99,9 +100,9 @@ export async function getRSNFormat(
if (anchor) { if (anchor) {
return rsnFromElement(anchor); return rsnFromElement(anchor);
} }
throw Error(PLAYER_NOT_FOUND_ERROR); throw new PlayerNotFoundError();
} catch { } catch {
throw Error(PLAYER_NOT_FOUND_ERROR); throw new HiScoresError();
} }
} }
@@ -191,7 +192,7 @@ export function parseStats(csv: string): Stats {
splitCSV.length !== splitCSV.length !==
SKILLS.length + BH_MODES.length + CLUES.length + BOSSES.length + 5 SKILLS.length + BH_MODES.length + CLUES.length + BOSSES.length + 5
) { ) {
throw Error(INVALID_FORMAT_ERROR); throw new InvalidFormatError();
} }
const skillObjects: Skill[] = splitCSV const skillObjects: Skill[] = splitCSV
@@ -284,91 +285,77 @@ export async function getStats(
]; ];
const shouldGetFormattedRsn = options?.shouldGetFormattedRsn ?? true; const shouldGetFormattedRsn = options?.shouldGetFormattedRsn ?? true;
const mainRes = await httpGet<HiscoresResponse>( const main = await getOfficialStats(rsn, 'main', options?.axiosConfigs?.main);
getStatsURL('main', rsn, true),
options?.axiosConfigs?.main const getModeStats = async (
); mode: Extract<Gamemode, 'ironman' | 'hardcore' | 'ultimate'>
if (mainRes.status === 200) { ): Promise<HiscoresResponse | undefined> =>
const emptyResponse: AxiosResponse<HiscoresResponse> = { otherGamemodes.includes(mode)
status: 404, ? getOfficialStats(rsn, mode, options?.axiosConfigs?.[mode])
data: { skills: [], activities: [] }, .catch(() => undefined)
statusText: '',
headers: {},
config: {} as InternalAxiosRequestConfig
};
const getModeStats = async (
mode: Extract<Gamemode, 'ironman' | 'hardcore' | 'ultimate'>
): Promise<AxiosResponse<HiscoresResponse>> =>
otherGamemodes.includes(mode)
? httpGet<HiscoresResponse>(
getStatsURL(mode, rsn, true),
options?.axiosConfigs?.[mode]
).catch((err) => err)
: emptyResponse;
const formattedName = shouldGetFormattedRsn
? await getRSNFormat(rsn, options?.axiosConfigs?.rsn).catch(
() => undefined
)
: undefined; : undefined;
const formattedName = shouldGetFormattedRsn
? await getRSNFormat(rsn, options?.axiosConfigs?.rsn).catch(
() => undefined
)
: undefined;
const player: Player = { const player: Player = {
name: formattedName ?? rsn, name: formattedName ?? rsn,
mode: 'main', mode: 'main',
dead: false, dead: false,
deulted: false, deulted: false,
deironed: false deironed: false
}; };
player.main = parseJsonStats(mainRes.data); player.main = parseJsonStats(main);
const ironRes = await getModeStats('ironman'); const iron = await getModeStats('ironman');
if (ironRes.status === 200) { if (iron) {
player.ironman = parseJsonStats(ironRes.data); player.ironman = parseJsonStats(iron);
const hcRes = await getModeStats('hardcore'); const hc = await getModeStats('hardcore');
const ultRes = await getModeStats('ultimate'); const ult = await getModeStats('ultimate');
if (hcRes.status === 200) { if (hc) {
player.mode = 'hardcore'; player.mode = 'hardcore';
player.hardcore = parseJsonStats(hcRes.data); player.hardcore = parseJsonStats(hc);
if ( if (
player.ironman.skills.overall.xp !== player.hardcore.skills.overall.xp player.ironman.skills.overall.xp !== player.hardcore.skills.overall.xp
) { ) {
player.dead = true; player.dead = true;
player.mode = 'ironman';
}
if (
player.main.skills.overall.xp !== player.ironman.skills.overall.xp
) {
player.deironed = true;
player.mode = 'main';
}
} else if (ultRes.status === 200) {
player.mode = 'ultimate';
player.ultimate = parseJsonStats(ultRes.data);
if (
player.ironman.skills.overall.xp !== player.ultimate.skills.overall.xp
) {
player.deulted = true;
player.mode = 'ironman';
}
if (
player.main.skills.overall.xp !== player.ironman.skills.overall.xp
) {
player.deironed = true;
player.mode = 'main';
}
} else {
player.mode = 'ironman'; player.mode = 'ironman';
if ( }
if (
player.main.skills.overall.xp !== player.ironman.skills.overall.xp
) {
player.deironed = true;
player.mode = 'main';
}
} else if (ult) {
player.mode = 'ultimate';
player.ultimate = parseJsonStats(ult);
if (
player.ironman.skills.overall.xp !== player.ultimate.skills.overall.xp
) {
player.deulted = true;
player.mode = 'ironman';
}
if (
player.main.skills.overall.xp !== player.ironman.skills.overall.xp
) {
player.deironed = true;
player.mode = 'main';
}
} else {
player.mode = 'ironman';
if (
player.main.skills.overall.xp !== player.ironman.skills.overall.xp player.main.skills.overall.xp !== player.ironman.skills.overall.xp
) { ) {
player.deironed = true; player.deironed = true;
player.mode = 'main'; player.mode = 'main';
}
} }
} }
return player;
} }
throw Error(PLAYER_NOT_FOUND_ERROR);
return player;
} }
/** /**

View File

@@ -282,3 +282,40 @@ export const FORMATTED_RIFTS_CLOSED = 'Rifts closed';
export const INVALID_FORMAT_ERROR = 'Invalid hiscores format'; export const INVALID_FORMAT_ERROR = 'Invalid hiscores format';
export const PLAYER_NOT_FOUND_ERROR = 'Player not found'; export const PLAYER_NOT_FOUND_ERROR = 'Player not found';
export const HISCORES_ERROR = 'HiScores not responding';
export class InvalidFormatError extends Error {
__proto__ = Error;
constructor() {
super(INVALID_FORMAT_ERROR);
Object.setPrototypeOf(this, InvalidFormatError.prototype);
}
}
export class InvalidRSNError extends Error {
__proto__ = Error;
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, InvalidRSNError.prototype);
}
}
export class PlayerNotFoundError extends Error {
__proto__ = Error;
constructor() {
super(PLAYER_NOT_FOUND_ERROR);
Object.setPrototypeOf(this, PlayerNotFoundError.prototype);
}
}
export class HiScoresError extends Error {
__proto__ = Error;
constructor() {
super(HISCORES_ERROR);
Object.setPrototypeOf(this, HiScoresError.prototype);
}
}

View File

@@ -7,7 +7,8 @@ import {
SCORES_URL, SCORES_URL,
SKILLS, SKILLS,
ACTIVITIES, ACTIVITIES,
JSON_STATS_URL JSON_STATS_URL,
InvalidRSNError
} from './constants'; } from './constants';
/** /**
@@ -119,10 +120,10 @@ export const httpGet = <Response>(
*/ */
export const validateRSN = (rsn: string) => { export const validateRSN = (rsn: string) => {
if (typeof rsn !== 'string') { if (typeof rsn !== 'string') {
throw Error('RSN must be a string'); throw new InvalidRSNError('RSN must be a string');
} else if (!/^[a-zA-Z0-9 _-]+$/.test(rsn)) { } else if (!/^[a-zA-Z0-9 _-]+$/.test(rsn)) {
throw Error('RSN contains invalid character'); throw new InvalidRSNError('RSN contains invalid character');
} else if (rsn.length > 12 || rsn.length < 1) { } else if (rsn.length > 12 || rsn.length < 1) {
throw Error('RSN must be between 1 and 12 characters'); throw new InvalidRSNError('RSN must be between 1 and 12 characters');
} }
}; };