263 lines
8.1 KiB
TypeScript
263 lines
8.1 KiB
TypeScript
import {
|
|
mapCalculateScoresResponse,
|
|
mapFinishGameResponse,
|
|
mapHealthResponse,
|
|
mapJoinSessionResponse,
|
|
mapMixAnswersResponse,
|
|
mapScoreboardResponse,
|
|
mapSessionDetailResponse,
|
|
mapShowQuestionResponse,
|
|
mapStartNextRoundResponse,
|
|
mapStartRoundResponse,
|
|
mapSubmitGuessResponse,
|
|
mapSubmitLieResponse
|
|
} from './mappers';
|
|
import type {
|
|
ApiFailure,
|
|
ApiResult,
|
|
CalculateScoresResponse,
|
|
FinishGameResponse,
|
|
HealthResponse,
|
|
JoinSessionRequest,
|
|
JoinSessionResponse,
|
|
MixAnswersResponse,
|
|
ScoreboardResponse,
|
|
SessionDetailResponse,
|
|
ShowQuestionResponse,
|
|
StartNextRoundResponse,
|
|
StartRoundRequest,
|
|
StartRoundResponse,
|
|
SubmitGuessRequest,
|
|
SubmitGuessResponse,
|
|
SubmitLieRequest,
|
|
SubmitLieResponse
|
|
} from './types';
|
|
|
|
export interface AngularHttpError {
|
|
status?: number;
|
|
message?: string;
|
|
error?: unknown;
|
|
}
|
|
|
|
export interface AngularHttpClientLike {
|
|
get<T>(url: string, options?: { withCredentials?: boolean }): Promise<T>;
|
|
post<T>(url: string, body: unknown, options?: { withCredentials?: boolean }): Promise<T>;
|
|
}
|
|
|
|
export interface AngularApiClient {
|
|
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>>;
|
|
}
|
|
|
|
function toFailure(error: unknown): ApiFailure {
|
|
const candidate = (error ?? {}) as AngularHttpError;
|
|
const status = typeof candidate.status === 'number' ? candidate.status : 0;
|
|
const payload = candidate.error;
|
|
|
|
if (status === 0) {
|
|
return {
|
|
kind: 'network',
|
|
status: 0,
|
|
message: candidate.message ?? 'Network error while contacting API'
|
|
};
|
|
}
|
|
|
|
return {
|
|
kind: 'http',
|
|
status,
|
|
message: candidate.message ?? `HTTP ${status}`,
|
|
...(payload === undefined ? {} : { payload })
|
|
};
|
|
}
|
|
|
|
function normalizeCode(code: string): string {
|
|
return code.trim().toUpperCase();
|
|
}
|
|
|
|
function normalizeBaseUrl(baseUrl: string): string {
|
|
return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
}
|
|
|
|
function buildUrl(baseUrl: string, path: string): string {
|
|
return `${normalizeBaseUrl(baseUrl)}${path}`;
|
|
}
|
|
|
|
async function wrap<T>(call: () => Promise<unknown>, mapper: (payload: unknown) => T): Promise<ApiResult<T>> {
|
|
let payload: unknown;
|
|
try {
|
|
payload = await call();
|
|
} catch (error: unknown) {
|
|
return {
|
|
ok: false,
|
|
status: typeof (error as AngularHttpError)?.status === 'number' ? (error as AngularHttpError).status! : 0,
|
|
error: toFailure(error)
|
|
};
|
|
}
|
|
|
|
try {
|
|
return { ok: true, status: 200, data: mapper(payload) };
|
|
} catch (error: unknown) {
|
|
return {
|
|
ok: false,
|
|
status: 200,
|
|
error: {
|
|
kind: 'parse',
|
|
status: 200,
|
|
message: error instanceof Error ? error.message : 'Invalid API response contract',
|
|
payload
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
export function createAngularApiClient(http: AngularHttpClientLike, baseUrl = ''): AngularApiClient {
|
|
return {
|
|
health: () =>
|
|
wrap(() => http.get<HealthResponse>(buildUrl(baseUrl, '/healthz'), { withCredentials: true }), mapHealthResponse),
|
|
getSession: (code: string) =>
|
|
wrap(
|
|
() =>
|
|
http.get<SessionDetailResponse>(buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}`), {
|
|
withCredentials: true
|
|
}),
|
|
mapSessionDetailResponse
|
|
),
|
|
joinSession: (payload: JoinSessionRequest) =>
|
|
wrap(
|
|
() =>
|
|
http.post<JoinSessionResponse>(
|
|
buildUrl(baseUrl, '/lobby/sessions/join'),
|
|
{
|
|
code: normalizeCode(payload.code),
|
|
nickname: payload.nickname.trim()
|
|
},
|
|
{ withCredentials: true }
|
|
),
|
|
mapJoinSessionResponse
|
|
),
|
|
startRound: (code: string, payload: StartRoundRequest) =>
|
|
wrap(
|
|
() =>
|
|
http.post<StartRoundResponse>(
|
|
buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/rounds/start`),
|
|
payload,
|
|
{ withCredentials: true }
|
|
),
|
|
mapStartRoundResponse
|
|
),
|
|
showQuestion: (code: string) =>
|
|
wrap(
|
|
() =>
|
|
http.post<ShowQuestionResponse>(
|
|
buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/show`),
|
|
{},
|
|
{ withCredentials: true }
|
|
),
|
|
mapShowQuestionResponse
|
|
),
|
|
mixAnswers: (code: string, roundQuestionId: number) =>
|
|
wrap(
|
|
() =>
|
|
http.post<MixAnswersResponse>(
|
|
buildUrl(
|
|
baseUrl,
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/answers/mix`
|
|
),
|
|
{},
|
|
{ withCredentials: true }
|
|
),
|
|
mapMixAnswersResponse
|
|
),
|
|
calculateScores: (code: string, roundQuestionId: number) =>
|
|
wrap(
|
|
() =>
|
|
http.post<CalculateScoresResponse>(
|
|
buildUrl(
|
|
baseUrl,
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/scores/calculate`
|
|
),
|
|
{},
|
|
{ withCredentials: true }
|
|
),
|
|
mapCalculateScoresResponse
|
|
),
|
|
getScoreboard: (code: string) =>
|
|
wrap(
|
|
() =>
|
|
http.get<ScoreboardResponse>(
|
|
buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/scoreboard`),
|
|
{ withCredentials: true }
|
|
),
|
|
mapScoreboardResponse
|
|
),
|
|
startNextRound: (code: string) =>
|
|
wrap(
|
|
() =>
|
|
http.post<StartNextRoundResponse>(
|
|
buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/rounds/next`),
|
|
{},
|
|
{ withCredentials: true }
|
|
),
|
|
mapStartNextRoundResponse
|
|
),
|
|
finishGame: (code: string) =>
|
|
wrap(
|
|
() =>
|
|
http.post<FinishGameResponse>(
|
|
buildUrl(baseUrl, `/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/finish`),
|
|
{},
|
|
{ withCredentials: true }
|
|
),
|
|
mapFinishGameResponse
|
|
),
|
|
submitLie: (code: string, roundQuestionId: number, payload: SubmitLieRequest) =>
|
|
wrap(
|
|
() =>
|
|
http.post<SubmitLieResponse>(
|
|
buildUrl(
|
|
baseUrl,
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/lies/submit`
|
|
),
|
|
{
|
|
player_id: payload.player_id,
|
|
session_token: payload.session_token,
|
|
text: payload.text
|
|
},
|
|
{ withCredentials: true }
|
|
),
|
|
mapSubmitLieResponse
|
|
),
|
|
submitGuess: (code: string, roundQuestionId: number, payload: SubmitGuessRequest) =>
|
|
wrap(
|
|
() =>
|
|
http.post<SubmitGuessResponse>(
|
|
buildUrl(
|
|
baseUrl,
|
|
`/lobby/sessions/${encodeURIComponent(normalizeCode(code))}/questions/${roundQuestionId}/guesses/submit`
|
|
),
|
|
{
|
|
player_id: payload.player_id,
|
|
session_token: payload.session_token,
|
|
selected_text: payload.selected_text
|
|
},
|
|
{ withCredentials: true }
|
|
),
|
|
mapSubmitGuessResponse
|
|
)
|
|
};
|
|
}
|