Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ContestType" ADD VALUE 'AOJ_ICPC';
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ enum ContestType {
OTHERS // AtCoder (その他)
AOJ_COURSES // AIZU ONLINE JUDGE Courses
AOJ_PCK // All-Japan High School Programming Contest (PCK)
AOJ_ICPC // ICPC (International Collegiate Programming Contest)
AOJ_JAG // ACM-ICPC Japan Alumni Group Contest (JAG)
}

Expand Down
63 changes: 63 additions & 0 deletions src/lib/clients/aizu_online_judge/clients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ const FIXTURE_PATHS = {
jagRegional: {
contests: './src/lib/clients/fixtures/aizu_online_judge/challenges/jag_regional/contests.json',
},
icpcPrelim: {
contests: './src/lib/clients/fixtures/aizu_online_judge/challenges/icpc_prelim/contests.json',
},
icpcRegional: {
contests: './src/lib/clients/fixtures/aizu_online_judge/challenges/icpc_regional/contests.json',
},
};

describe('AojCoursesApiClient', () => {
Expand Down Expand Up @@ -251,4 +257,61 @@ describe('AojChallengesApiClient', () => {
expect(tasks.length).toBe(expectedCount);
});
});

describe('ICPC PRELIM', () => {
const contestsMock = loadMockData<AOJChallengeContestAPI>(FIXTURE_PATHS.icpcPrelim.contests);
let client: AojChallengesApiClient;

beforeEach(() => {
nock(AOJ_API_BASE).get('/challenges/cl/ICPC/PRELIM').reply(200, contestsMock);
client = buildChallengesClient();
});

test('fetches and transforms ICPC PRELIM contests', async () => {
const contests = await client.getContests({ contestType: 'ICPC', round: 'PRELIM' });
const expectedCount = contestsMock.contests.flatMap((contest) => contest.days).length;
expect(contests.length).toBe(expectedCount);
});

test('fetches and transforms ICPC PRELIM tasks', async () => {
const tasks = await client.getTasks({ contestType: 'ICPC', round: 'PRELIM' });
const expectedCount = contestsMock.contests
.flatMap((contest) => contest.days)
.flatMap((day) => day.problems).length;
expect(tasks.length).toBe(expectedCount);
});
});

describe('ICPC REGIONAL', () => {
const contestsMock = loadMockData<AOJChallengeContestAPI>(FIXTURE_PATHS.icpcRegional.contests);
let client: AojChallengesApiClient;

beforeEach(() => {
nock(AOJ_API_BASE).get('/challenges/cl/ICPC/REGIONAL').reply(200, contestsMock);
client = buildChallengesClient();
});

test('fetches and transforms ICPC REGIONAL contests', async () => {
const contests = await client.getContests({ contestType: 'ICPC', round: 'REGIONAL' });
const expectedCount = contestsMock.contests.flatMap((contest) => contest.days).length;
expect(contests.length).toBe(expectedCount);
});

test('fetches and transforms ICPC REGIONAL tasks', async () => {
const tasks = await client.getTasks({ contestType: 'ICPC', round: 'REGIONAL' });
const expectedCount = contestsMock.contests
.flatMap((contest) => contest.days)
.flatMap((day) => day.problems).length;
expect(tasks.length).toBe(expectedCount);
});

test('each ICPC REGIONAL task has required fields', async () => {
const tasks = await client.getTasks({ contestType: 'ICPC', round: 'REGIONAL' });
tasks.forEach((task) => {
expect(task.id).toBeDefined();
expect(task.contest_id).toBeDefined();
expect(task.title).toBeDefined();
});
});
});
});
11 changes: 3 additions & 8 deletions src/lib/clients/aizu_online_judge/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import type {
AOJChallengeContestAPI,
ChallengeContest,
ChallengeParams,
ChallengeContestType,
ChallengeRoundMap,
} from './types';
import { buildEndpoint, mapToContest, mapToTask, getCourseName } from './utils';

Expand Down Expand Up @@ -67,7 +65,7 @@ export class AojChallengesApiClient {
const { contestType, round } = params;

return getCachedOrFetchContests<AOJChallengeContestAPI>(this.httpClient, this.cache, {
cacheKey: this.getCacheKey(contestType, round),
cacheKey: this.getCacheKey(params),
endpoint: buildEndpoint(['challenges', 'cl', contestType, round]),
errorMessage: `Failed to fetch ${contestType} ${round} contests from AOJ API`,
validateResponse: (data) =>
Expand All @@ -86,7 +84,7 @@ export class AojChallengesApiClient {
const { contestType, round } = params;

return getCachedOrFetchTasks<AOJChallengeContestAPI>(this.httpClient, this.cache, {
cacheKey: this.getCacheKey(contestType, round),
cacheKey: this.getCacheKey(params),
endpoint: buildEndpoint(['challenges', 'cl', contestType, round]),
errorMessage: `Failed to fetch ${contestType} ${round} tasks from AOJ API`,
validateResponse: (data) =>
Expand All @@ -101,10 +99,7 @@ export class AojChallengesApiClient {
});
}

private getCacheKey(
contestType: ChallengeContestType,
round: ChallengeRoundMap[ChallengeContestType],
): string {
private getCacheKey({ contestType, round }: ChallengeParams): string {
return `aoj_${contestType.toLowerCase()}_${round.toLowerCase()}`;
}
}
28 changes: 8 additions & 20 deletions src/lib/clients/aizu_online_judge/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,21 @@ export type Course = {

export type Courses = Course[];

/**
* Parameters for configuring a challenge contest in the AOJ.
* @property {ChallengeContestType} contestType - The type of contest for the challenge.
* @property {ChallengeRoundMap[ChallengeContestType]} round - The round of the contest.
*/
export type ChallengeParams = {
contestType: ChallengeContestType;
round: ChallengeRoundMap[ChallengeContestType];
};

/** Represents the types of challenge contests available. */
export type ChallengeContestType = 'PCK' | 'JAG';

/**
* A map that associates each type of challenge contest with its corresponding round type.
*/
export type ChallengeRoundMap = {
PCK: PckRound;
JAG: JagRound;
};
/** Discriminated union enforcing valid (contestType, round) pairs for AOJ challenge contests. */
export type ChallengeParams =
| { contestType: 'PCK'; round: PckRound }
| { contestType: 'JAG'; round: JagRound }
| { contestType: 'ICPC'; round: IcpcRound };

/** Represents PCK contest rounds */
export type PckRound = 'PRELIM' | 'FINAL';

/** Represents JAG contest rounds */
export type JagRound = 'PRELIM' | 'REGIONAL';

/** Represents ICPC contest rounds */
export type IcpcRound = 'PRELIM' | 'REGIONAL';

export type AOJChallengeContestAPI = {
readonly largeCl: Record<string, unknown>;
readonly contests: ChallengeContests;
Expand Down
Loading
Loading