aboutsummaryrefslogtreecommitdiffhomepage
path: root/typespec/api-server
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-14 20:32:47 +0900
committernsfisis <nsfisis@gmail.com>2026-02-14 20:32:47 +0900
commit9185367fcd7d95af89fac36dd892d8b064dbd94f (patch)
tree6085f0c4ab695d0f83348f32b49b5481f1da6548 /typespec/api-server
parent6bd35e345a4c5d74578b0f8a5c969027e7e15f02 (diff)
downloadphperkaigi-2026-albatross-9185367fcd7d95af89fac36dd892d8b064dbd94f.tar.gz
phperkaigi-2026-albatross-9185367fcd7d95af89fac36dd892d8b064dbd94f.tar.zst
phperkaigi-2026-albatross-9185367fcd7d95af89fac36dd892d8b064dbd94f.zip
feat(openapi): generate OpenAPI specs from TypeSpec sources
Migrate hand-written OpenAPI YAML to TypeSpec (.tsp) source files. TypeSpec compiles to OpenAPI 3.0 YAML, enabling type-safe API definitions. - Add typespec/ directory with api-server and fortee definitions - Integrate TypeSpec build into `just gen` and `just build` pipelines - Update backend handler code to match new generated type names (inlined error responses, separate GameType/ProblemLanguage enums) - Regenerate frontend TypeScript types from new OpenAPI output Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'typespec/api-server')
-rw-r--r--typespec/api-server/main.tsp17
-rw-r--r--typespec/api-server/models.tsp119
-rw-r--r--typespec/api-server/routes.tsp126
-rw-r--r--typespec/api-server/tspconfig.yaml8
4 files changed, 270 insertions, 0 deletions
diff --git a/typespec/api-server/main.tsp b/typespec/api-server/main.tsp
new file mode 100644
index 0000000..00d1816
--- /dev/null
+++ b/typespec/api-server/main.tsp
@@ -0,0 +1,17 @@
+import "@typespec/http";
+import "@typespec/openapi";
+import "@typespec/openapi3";
+
+import "./models.tsp";
+import "./routes.tsp";
+
+using TypeSpec.Http;
+using TypeSpec.OpenAPI;
+
+@service(#{
+ title: "Albatross internal web API",
+})
+@info(#{
+ version: "0.3.0",
+})
+namespace AlbatrossApi;
diff --git a/typespec/api-server/models.tsp b/typespec/api-server/models.tsp
new file mode 100644
index 0000000..47519be
--- /dev/null
+++ b/typespec/api-server/models.tsp
@@ -0,0 +1,119 @@
+using TypeSpec.Http;
+using TypeSpec.OpenAPI;
+
+namespace AlbatrossApi;
+
+// ---------- Error ----------
+
+model Error {
+ message: string;
+}
+
+// ---------- Error Responses ----------
+
+@error
+model UnauthorizedError {
+ @statusCode statusCode: 401;
+ @body body: Error;
+}
+
+@error
+model ForbiddenError {
+ @statusCode statusCode: 403;
+ @body body: Error;
+}
+
+@error
+model NotFoundError {
+ @statusCode statusCode: 404;
+ @body body: Error;
+}
+
+// ---------- Enums ----------
+
+enum GameType {
+ `1v1`,
+ multiplayer,
+}
+
+enum ProblemLanguage {
+ php,
+ swift,
+}
+
+enum ExecutionStatus {
+ none,
+ running,
+ success,
+ wrong_answer,
+ timeout,
+ compile_error,
+ runtime_error,
+ internal_error,
+}
+
+// ---------- Models ----------
+
+model User {
+ user_id: integer;
+ username: string;
+ display_name: string;
+ icon_path?: string;
+ is_admin: boolean;
+ label: string | null;
+}
+
+model Problem {
+ problem_id: integer;
+ title: string;
+ description: string;
+ language: ProblemLanguage;
+ sample_code: string;
+}
+
+model Game {
+ game_id: integer;
+ game_type: GameType;
+ is_public: boolean;
+ display_name: string;
+ duration_seconds: integer;
+
+ @extension("x-go-type", "int64")
+ started_at?: integer;
+
+ problem: Problem;
+ main_players: User[];
+}
+
+model LatestGameState {
+ code: string;
+ score: integer | null;
+
+ @extension("x-go-type", "int64")
+ best_score_submitted_at: integer | null;
+
+ status: ExecutionStatus;
+}
+
+model RankingEntry {
+ player: User;
+ score: integer;
+
+ @extension("x-go-type", "int64")
+ submitted_at: integer;
+
+ code: string | null;
+}
+
+model Tournament {
+ matches: TournamentMatch[];
+}
+
+model TournamentMatch {
+ game_id: integer;
+ player1?: User;
+ player2?: User;
+ player1_score?: integer;
+ player2_score?: integer;
+ winner?: integer;
+}
diff --git a/typespec/api-server/routes.tsp b/typespec/api-server/routes.tsp
new file mode 100644
index 0000000..3409cea
--- /dev/null
+++ b/typespec/api-server/routes.tsp
@@ -0,0 +1,126 @@
+using TypeSpec.Http;
+using TypeSpec.OpenAPI;
+
+namespace AlbatrossApi;
+
+// ---------- Auth ----------
+
+@route("/login")
+@post
+@operationId("postLogin")
+op postLogin(@body body: {
+ username: string;
+ password: string;
+}): {
+ @body body: {
+ user: User;
+ };
+} | UnauthorizedError;
+
+@route("/logout")
+@post
+@operationId("postLogout")
+op postLogout(): {
+ @statusCode statusCode: 200;
+} | UnauthorizedError;
+
+@route("/me")
+@get
+@operationId("getMe")
+op getMe(): {
+ @body body: {
+ user: User;
+ };
+} | UnauthorizedError;
+
+// ---------- Games ----------
+
+@route("/games")
+@get
+@operationId("getGames")
+op getGames(): {
+ @body body: {
+ games: Game[];
+ };
+} | UnauthorizedError | ForbiddenError;
+
+@route("/games/{game_id}")
+@get
+@operationId("getGame")
+op getGame(@path game_id: integer): {
+ @body body: {
+ game: Game;
+ };
+} | UnauthorizedError | ForbiddenError | NotFoundError;
+
+// ---------- Play ----------
+
+@route("/games/{game_id}/play/latest_state")
+@get
+@operationId("getGamePlayLatestState")
+op getGamePlayLatestState(@path game_id: integer): {
+ @body body: {
+ state: LatestGameState;
+ };
+} | UnauthorizedError | ForbiddenError | NotFoundError;
+
+@route("/games/{game_id}/play/code")
+@post
+@operationId("postGamePlayCode")
+op postGamePlayCode(
+ @path game_id: integer,
+ @body body: {
+ code: string;
+ },
+): {
+ @statusCode statusCode: 200;
+} | UnauthorizedError | ForbiddenError | NotFoundError;
+
+@route("/games/{game_id}/play/submit")
+@post
+@operationId("postGamePlaySubmit")
+op postGamePlaySubmit(
+ @path game_id: integer,
+ @body body: {
+ code: string;
+ },
+): {
+ @statusCode statusCode: 200;
+} | UnauthorizedError | ForbiddenError | NotFoundError;
+
+// ---------- Watch ----------
+
+@route("/games/{game_id}/watch/ranking")
+@get
+@operationId("getGameWatchRanking")
+op getGameWatchRanking(@path game_id: integer): {
+ @body body: {
+ ranking: RankingEntry[];
+ };
+} | UnauthorizedError | ForbiddenError | NotFoundError;
+
+@route("/games/{game_id}/watch/latest_states")
+@get
+@operationId("getGameWatchLatestStates")
+op getGameWatchLatestStates(@path game_id: integer): {
+ @body body: {
+ states: Record<LatestGameState>;
+ };
+} | UnauthorizedError | ForbiddenError | NotFoundError;
+
+// ---------- Tournament ----------
+
+@route("/tournament")
+@get
+@operationId("getTournament")
+op getTournament(
+ @query game1: integer,
+ @query game2: integer,
+ @query game3: integer,
+ @query game4: integer,
+ @query game5: integer,
+): {
+ @body body: {
+ tournament: Tournament;
+ };
+} | UnauthorizedError | ForbiddenError | NotFoundError;
diff --git a/typespec/api-server/tspconfig.yaml b/typespec/api-server/tspconfig.yaml
new file mode 100644
index 0000000..78ba744
--- /dev/null
+++ b/typespec/api-server/tspconfig.yaml
@@ -0,0 +1,8 @@
+output-dir: "{project-root}/tsp-output"
+emit:
+ - "@typespec/openapi3"
+options:
+ "@typespec/openapi3":
+ openapi-versions:
+ - "3.0.0"
+ output-file: openapi.yaml