diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-14 20:32:47 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-14 20:32:47 +0900 |
| commit | 9185367fcd7d95af89fac36dd892d8b064dbd94f (patch) | |
| tree | 6085f0c4ab695d0f83348f32b49b5481f1da6548 /typespec/api-server | |
| parent | 6bd35e345a4c5d74578b0f8a5c969027e7e15f02 (diff) | |
| download | phperkaigi-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.tsp | 17 | ||||
| -rw-r--r-- | typespec/api-server/models.tsp | 119 | ||||
| -rw-r--r-- | typespec/api-server/routes.tsp | 126 | ||||
| -rw-r--r-- | typespec/api-server/tspconfig.yaml | 8 |
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 |
