199 lines
6.4 KiB
TypeScript
199 lines
6.4 KiB
TypeScript
import {
|
|
mapCalculateScoresResponse,
|
|
mapFinishGameResponse,
|
|
mapHealthResponse,
|
|
mapJoinSessionResponse,
|
|
mapMixAnswersResponse,
|
|
mapScoreboardResponse,
|
|
mapSessionDetailResponse,
|
|
mapShowQuestionResponse,
|
|
mapStartNextRoundResponse,
|
|
mapStartRoundResponse,
|
|
mapSubmitGuessResponse,
|
|
mapSubmitLieResponse
|
|
} from './mappers';
|
|
import type {
|
|
ApiResult,
|
|
CalculateScoresResponse,
|
|
FinishGameResponse,
|
|
HealthResponse,
|
|
JoinSessionRequest,
|
|
JoinSessionResponse,
|
|
MixAnswersResponse,
|
|
ScoreboardResponse,
|
|
SessionDetailResponse,
|
|
ShowQuestionResponse,
|
|
StartNextRoundResponse,
|
|
StartRoundRequest,
|
|
StartRoundResponse,
|
|
SubmitGuessRequest,
|
|
SubmitGuessResponse,
|
|
SubmitLieRequest,
|
|
SubmitLieResponse
|
|
} from './types';
|
|
|
|
export interface ApiClient {
|
|
health(): Promise<ApiResult<HealthResponse>>;
|
|
getSession(code: string): Promise<ApiResult<SessionDetailResponse>>;
|
|
joinSession(payload: JoinSessionRequest): Promise<ApiResult<JoinSessionResponse>>;
|
|
startRound(code: string, payload: StartRoundRequest): Promise<ApiResult<StartRoundResponse>>;
|
|
showQuestion(code: string): Promise<ApiResult<ShowQuestionResponse>>;
|
|
mixAnswers(code: string, roundQuestionId: number): Promise<ApiResult<MixAnswersResponse>>;
|
|
calculateScores(code: string, roundQuestionId: number): Promise<ApiResult<CalculateScoresResponse>>;
|
|
getScoreboard(code: string): Promise<ApiResult<ScoreboardResponse>>;
|
|
startNextRound(code: string): Promise<ApiResult<StartNextRoundResponse>>;
|
|
finishGame(code: string): Promise<ApiResult<FinishGameResponse>>;
|
|
submitLie(code: string, roundQuestionId: number, payload: SubmitLieRequest): Promise<ApiResult<SubmitLieResponse>>;
|
|
submitGuess(code: string, roundQuestionId: number, payload: SubmitGuessRequest): Promise<ApiResult<SubmitGuessResponse>>;
|
|
}
|
|
|
|
export function createApiClient(baseUrl = '', fetchImpl: typeof fetch = fetch): ApiClient {
|
|
async function request<T>(
|
|
path: string,
|
|
method: 'GET' | 'POST',
|
|
mapper: (payload: unknown) => T,
|
|
payload?: unknown
|
|
): Promise<ApiResult<T>> {
|
|
let response: Response;
|
|
try {
|
|
response = await fetchImpl(`${baseUrl}${path}`, {
|
|
method,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
...(payload === undefined ? {} : { 'Content-Type': 'application/json' })
|
|
},
|
|
...(payload === undefined ? {} : { body: JSON.stringify(payload) })
|
|
});
|
|
} catch {
|
|
return {
|
|
ok: false,
|
|
status: 0,
|
|
error: { kind: 'network', status: 0, message: 'Network error while contacting API' }
|
|
};
|
|
}
|
|
|
|
let responsePayload: unknown;
|
|
try {
|
|
responsePayload = await response.json();
|
|
} catch {
|
|
return {
|
|
ok: false,
|
|
status: response.status,
|
|
error: { kind: 'parse', status: response.status, message: 'Invalid JSON response from API' }
|
|
};
|
|
}
|
|
|
|
if (!response.ok) {
|
|
return {
|
|
ok: false,
|
|
status: response.status,
|
|
error: {
|
|
kind: 'http',
|
|
status: response.status,
|
|
message: `HTTP ${response.status}`,
|
|
payload: responsePayload
|
|
}
|
|
};
|
|
}
|
|
|
|
try {
|
|
return { ok: true, status: response.status, data: mapper(responsePayload) };
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
status: response.status,
|
|
error: {
|
|
kind: 'parse',
|
|
status: response.status,
|
|
message: error instanceof Error ? error.message : 'Invalid API response contract',
|
|
payload: responsePayload
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
const normalizeCode = (value: string): string => value.trim().toUpperCase();
|
|
|
|
return {
|
|
health: () => request<HealthResponse>('/healthz', 'GET', mapHealthResponse),
|
|
getSession: (code: string) =>
|
|
request<SessionDetailResponse>(
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}`,
|
|
'GET',
|
|
mapSessionDetailResponse
|
|
),
|
|
joinSession: (payload: JoinSessionRequest) =>
|
|
request<JoinSessionResponse>(
|
|
'/lobby/sessions/join',
|
|
'POST',
|
|
mapJoinSessionResponse,
|
|
{
|
|
code: normalizeCode(payload.code),
|
|
nickname: payload.nickname.trim()
|
|
}
|
|
),
|
|
startRound: (code: string, payload: StartRoundRequest) =>
|
|
request<StartRoundResponse>(
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/rounds/start`,
|
|
'POST',
|
|
mapStartRoundResponse,
|
|
payload
|
|
),
|
|
showQuestion: (code: string) =>
|
|
request<ShowQuestionResponse>(
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/show`,
|
|
'POST',
|
|
mapShowQuestionResponse,
|
|
{}
|
|
),
|
|
mixAnswers: (code: string, roundQuestionId: number) =>
|
|
request<MixAnswersResponse>(
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/answers/mix`,
|
|
'POST',
|
|
mapMixAnswersResponse,
|
|
{}
|
|
),
|
|
calculateScores: (code: string, roundQuestionId: number) =>
|
|
request<CalculateScoresResponse>(
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/scores/calculate`,
|
|
'POST',
|
|
mapCalculateScoresResponse,
|
|
{}
|
|
),
|
|
getScoreboard: (code: string) =>
|
|
request<ScoreboardResponse>(
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/scoreboard`,
|
|
'GET',
|
|
mapScoreboardResponse
|
|
),
|
|
startNextRound: (code: string) =>
|
|
request<StartNextRoundResponse>(
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/rounds/next`,
|
|
'POST',
|
|
mapStartNextRoundResponse,
|
|
{}
|
|
),
|
|
finishGame: (code: string) =>
|
|
request<FinishGameResponse>(
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/finish`,
|
|
'POST',
|
|
mapFinishGameResponse,
|
|
{}
|
|
),
|
|
submitLie: (code: string, roundQuestionId: number, payload: SubmitLieRequest) =>
|
|
request<SubmitLieResponse>(
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/lies/submit`,
|
|
'POST',
|
|
mapSubmitLieResponse,
|
|
payload
|
|
),
|
|
submitGuess: (code: string, roundQuestionId: number, payload: SubmitGuessRequest) =>
|
|
request<SubmitGuessResponse>(
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/guesses/submit`,
|
|
'POST',
|
|
mapSubmitGuessResponse,
|
|
payload
|
|
)
|
|
};
|
|
}
|