From 4484ffa9959fa3203b804012868a9f33bd4bcf15 Mon Sep 17 00:00:00 2001 From: LQ63 Date: Sun, 12 Apr 2026 16:27:27 +0200 Subject: [PATCH] docs(core): NowChess API Spec Added an API Spec to the docs folder --- docs/api-spec.yaml | 776 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 776 insertions(+) create mode 100644 docs/api-spec.yaml diff --git a/docs/api-spec.yaml b/docs/api-spec.yaml new file mode 100644 index 0000000..596520d --- /dev/null +++ b/docs/api-spec.yaml @@ -0,0 +1,776 @@ +openapi: 3.0.3 +info: + title: NowChess API + description: | + REST API for the NowChess application. Designed to feel familiar to users + of the [lichess API](https://lichess.org/api). + + ## Authentication + Most endpoints require a Bearer token: + ``` + Authorization: Bearer + ``` + Authentication is reserved for future implementation — endpoints are currently + open unless noted otherwise. + + ## Move notation + Moves are expressed in **UCI notation**: `{from}{to}[promotion]` + - Normal move: `e2e4` + - Capture: `d5e6` + - Promotion: `e7e8q` (q=queen, r=rook, b=bishop, n=knight) + - Castling: `e1g1` (kingside white), `e1c1` (queenside white) + + ## Streaming + Endpoints that support streaming return **NDJSON** (newline-delimited JSON). + Request them with: + ``` + Accept: application/x-ndjson + ``` + Each line of the response is a complete JSON object. Empty lines are + keep-alive heartbeats. + + ## Rate limiting + Requests that exceed the rate limit receive `429 Too Many Requests`. + Honour the `Retry-After` response header and wait before retrying. + version: 1.0.0 + contact: + name: NowChess + license: + name: MIT + +servers: + - url: http://localhost:8080 + description: Local development server + +tags: + - name: game + description: Create and manage chess games + - name: move + description: Make moves and navigate game history + - name: draw + description: Draw offers and claims + - name: import + description: Load a game from FEN or PGN + - name: export + description: Export a game as FEN or PGN + +paths: + + # --------------------------------------------------------------------------- + # Game lifecycle + # --------------------------------------------------------------------------- + + /api/game: + post: + operationId: createGame + tags: [game] + summary: Create a new game + description: | + Creates a new chess game starting from the initial position. + Returns the full game state including the generated `gameId`. + security: + - bearerAuth: [] + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/CreateGameRequest' + responses: + '201': + description: Game created + content: + application/json: + schema: + $ref: '#/components/schemas/GameFull' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/TooManyRequests' + + /api/game/{gameId}: + get: + operationId: getGame + tags: [game] + summary: Get game state + description: Returns the full current state of a game. + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/gameId' + responses: + '200': + description: Current game state + content: + application/json: + schema: + $ref: '#/components/schemas/GameFull' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + + /api/game/{gameId}/stream: + get: + operationId: streamGame + tags: [game] + summary: Stream game events + description: | + Opens a persistent NDJSON stream for a game. The first object sent is + a `gameFull` event containing the complete game state. Subsequent + objects are `gameState` events sent whenever the game changes (move + made, draw offered, game over, etc.). + + Empty lines are heartbeats to keep the connection alive. + + Connect with: + ``` + Accept: application/x-ndjson + ``` + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/gameId' + responses: + '200': + description: NDJSON event stream + content: + application/x-ndjson: + schema: + oneOf: + - $ref: '#/components/schemas/GameFullEvent' + - $ref: '#/components/schemas/GameStateEvent' + - $ref: '#/components/schemas/ErrorEvent' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + + /api/game/{gameId}/resign: + post: + operationId: resignGame + tags: [game] + summary: Resign the game + description: The active player resigns. The game ends immediately. + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/gameId' + responses: + '200': + description: Resignation accepted + content: + application/json: + schema: + $ref: '#/components/schemas/OkResponse' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + + # --------------------------------------------------------------------------- + # Move-making + # --------------------------------------------------------------------------- + + /api/game/{gameId}/move/{uci}: + post: + operationId: makeMove + tags: [move] + summary: Make a move + description: | + Submit a move in UCI notation. The move must be legal for the side + currently to move. + + For promotion moves include the target piece as the fifth character: + `e7e8q`, `a2a1r`, etc. + + If the move results in a pawn reaching the back rank and no promotion + character is supplied, the game enters `promotionPending` status and + the move is not yet applied — resubmit with the promotion character. + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/gameId' + - name: uci + in: path + required: true + description: Move in UCI notation (e.g. `e2e4`, `e7e8q`) + schema: + type: string + pattern: '^[a-h][1-8][a-h][1-8][qrbn]?$' + example: e2e4 + responses: + '200': + description: Move applied — returns updated game state + content: + application/json: + schema: + $ref: '#/components/schemas/GameState' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + + /api/game/{gameId}/moves: + get: + operationId: getLegalMoves + tags: [move] + summary: Get legal moves + description: | + Returns all legal moves for the side currently to move. + Optionally filter to moves originating from a single square. + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/gameId' + - name: square + in: query + required: false + description: Filter to moves from this square (e.g. `e2`) + schema: + type: string + pattern: '^[a-h][1-8]$' + example: e2 + responses: + '200': + description: List of legal moves + content: + application/json: + schema: + $ref: '#/components/schemas/LegalMovesResponse' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + + /api/game/{gameId}/undo: + post: + operationId: undoMove + tags: [move] + summary: Undo the last move + description: Reverts the most recent move. Returns the updated game state. + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/gameId' + responses: + '200': + description: Move undone + content: + application/json: + schema: + $ref: '#/components/schemas/GameState' + '400': + description: No moves to undo + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + + /api/game/{gameId}/redo: + post: + operationId: redoMove + tags: [move] + summary: Redo a previously undone move + description: Re-applies the next move in the undo stack. Returns the updated game state. + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/gameId' + responses: + '200': + description: Move redone + content: + application/json: + schema: + $ref: '#/components/schemas/GameState' + '400': + description: No moves to redo + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + + # --------------------------------------------------------------------------- + # Draw handling + # --------------------------------------------------------------------------- + + /api/game/{gameId}/draw/{action}: + post: + operationId: drawAction + tags: [draw] + summary: Offer, accept, decline, or claim a draw + description: | + Perform a draw-related action: + + | Action | Description | + |-----------|-------------| + | `offer` | Offer a draw to the opponent | + | `accept` | Accept the opponent's draw offer | + | `decline` | Decline the opponent's draw offer | + | `claim` | Claim a draw under the fifty-move rule (only valid when `status` is `fiftyMoveAvailable`) | + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/gameId' + - name: action + in: path + required: true + schema: + type: string + enum: [offer, accept, decline, claim] + responses: + '200': + description: Action accepted + content: + application/json: + schema: + $ref: '#/components/schemas/OkResponse' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + + # --------------------------------------------------------------------------- + # Import + # --------------------------------------------------------------------------- + + /api/game/import/fen: + post: + operationId: importFen + tags: [import] + summary: Load a position from FEN + description: | + Creates a new game from a FEN string. The game starts at the position + described by the FEN; move history prior to that position is not + available. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportFenRequest' + responses: + '201': + description: Game created from FEN + content: + application/json: + schema: + $ref: '#/components/schemas/GameFull' + '400': + $ref: '#/components/responses/BadRequest' + '429': + $ref: '#/components/responses/TooManyRequests' + + /api/game/import/pgn: + post: + operationId: importPgn + tags: [import] + summary: Load a game from PGN + description: | + Creates a new game by replaying all moves in a PGN string. The game + starts at the position after the final move in the PGN; undo is + available for every replayed move. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportPgnRequest' + responses: + '201': + description: Game created from PGN + content: + application/json: + schema: + $ref: '#/components/schemas/GameFull' + '400': + $ref: '#/components/responses/BadRequest' + '429': + $ref: '#/components/responses/TooManyRequests' + + # --------------------------------------------------------------------------- + # Export + # --------------------------------------------------------------------------- + + /api/game/{gameId}/export/fen: + get: + operationId: exportFen + tags: [export] + summary: Export current position as FEN + description: Returns the FEN string representing the current board position. + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/gameId' + responses: + '200': + description: FEN string + content: + text/plain: + schema: + type: string + example: rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1 + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + + /api/game/{gameId}/export/pgn: + get: + operationId: exportPgn + tags: [export] + summary: Export game as PGN + description: Returns the full PGN for the game including headers and move text. + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/gameId' + responses: + '200': + description: PGN text + content: + application/x-chess-pgn: + schema: + type: string + example: | + [Event "NowChess game"] + [White "Player1"] + [Black "Player2"] + [Result "*"] + + 1. e4 e5 2. Nf3 * + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + +# ============================================================================= +# Components +# ============================================================================= + +components: + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + description: 'Personal access token — `Authorization: Bearer `' + + parameters: + gameId: + name: gameId + in: path + required: true + description: 8-character alphanumeric game ID (e.g. `Qa7FJNk2`) + schema: + type: string + pattern: '^[A-Za-z0-9]{8}$' + example: Qa7FJNk2 + + responses: + BadRequest: + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + Unauthorized: + description: Missing or invalid authentication token + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + NotFound: + description: Game not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + TooManyRequests: + description: Rate limit exceeded — see `Retry-After` header + headers: + Retry-After: + description: Seconds to wait before retrying + schema: + type: integer + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + schemas: + + # ------------------------------------------------------------------------- + # Requests + # ------------------------------------------------------------------------- + + CreateGameRequest: + type: object + description: Parameters for creating a new game. All fields are optional. + properties: + white: + $ref: '#/components/schemas/PlayerInfo' + black: + $ref: '#/components/schemas/PlayerInfo' + + ImportFenRequest: + type: object + required: [fen] + properties: + fen: + type: string + description: Complete FEN string (6 fields) + example: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + white: + $ref: '#/components/schemas/PlayerInfo' + black: + $ref: '#/components/schemas/PlayerInfo' + + ImportPgnRequest: + type: object + required: [pgn] + properties: + pgn: + type: string + description: PGN text (headers and move list) + example: "1. e4 e5 2. Nf3 Nc6 *" + + # ------------------------------------------------------------------------- + # Game state + # ------------------------------------------------------------------------- + + GameFull: + type: object + description: Complete game information including players and current state. + required: [gameId, white, black, state] + properties: + gameId: + type: string + description: Unique 8-character game identifier + example: Qa7FJNk2 + white: + $ref: '#/components/schemas/PlayerInfo' + black: + $ref: '#/components/schemas/PlayerInfo' + state: + $ref: '#/components/schemas/GameState' + + GameState: + type: object + description: | + The current game state. Included in `GameFull` and returned by move + endpoints and stream events. + required: [fen, pgn, turn, status, moves, undoAvailable, redoAvailable] + properties: + fen: + type: string + description: FEN string for the current position + example: rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1 + pgn: + type: string + description: PGN move text for the full game so far + example: "1. e4" + turn: + type: string + enum: [white, black] + description: The side to move + status: + $ref: '#/components/schemas/GameStatus' + winner: + type: string + enum: [white, black] + description: Set when `status` is `checkmate` or `resign` + nullable: true + moves: + type: array + description: All moves played so far, in UCI notation + items: + type: string + example: [e2e4, e7e5, g1f3] + undoAvailable: + type: boolean + description: Whether `POST /undo` is currently valid + redoAvailable: + type: boolean + description: Whether `POST /redo` is currently valid + + GameStatus: + type: string + description: | + Current game status: + + | Value | Meaning | + |-------|---------| + | `started` | Game in progress, no special condition | + | `check` | Side to move is in check | + | `checkmate` | Side to move is checkmated — game over | + | `stalemate` | Side to move has no legal moves, not in check — game over (draw) | + | `resign` | A player resigned — game over | + | `draw` | Draw agreed or claimed — game over | + | `drawOffered` | Waiting for the opponent to accept or decline a draw offer | + | `fiftyMoveAvailable` | Fifty-move rule threshold reached; active player may claim draw | + | `promotionPending` | A pawn reached the back rank; awaiting promotion piece selection | + | `insufficientMaterial` | Neither side has enough pieces to deliver checkmate — game over (draw) | + enum: + - started + - check + - checkmate + - stalemate + - resign + - draw + - drawOffered + - fiftyMoveAvailable + - promotionPending + - insufficientMaterial + + # ------------------------------------------------------------------------- + # Moves + # ------------------------------------------------------------------------- + + LegalMovesResponse: + type: object + required: [moves] + properties: + moves: + type: array + items: + $ref: '#/components/schemas/LegalMove' + + LegalMove: + type: object + required: [from, to, uci, moveType] + properties: + from: + type: string + description: Origin square in algebraic notation + example: e2 + to: + type: string + description: Destination square in algebraic notation + example: e4 + uci: + type: string + description: Full move in UCI notation + example: e2e4 + moveType: + $ref: '#/components/schemas/MoveType' + promotion: + type: string + enum: [queen, rook, bishop, knight] + description: Target piece for promotion moves + nullable: true + + MoveType: + type: string + description: Classification of the move + enum: + - normal + - capture + - castleKingside + - castleQueenside + - enPassant + - promotion + + # ------------------------------------------------------------------------- + # Streaming events + # ------------------------------------------------------------------------- + + GameFullEvent: + type: object + description: | + First event on a game stream. Contains the complete game snapshot. + required: [type, game] + properties: + type: + type: string + enum: [gameFull] + game: + $ref: '#/components/schemas/GameFull' + + GameStateEvent: + type: object + description: | + Emitted on a game stream whenever the game state changes (move played, + draw offered, game over, etc.). + required: [type, state] + properties: + type: + type: string + enum: [gameState] + state: + $ref: '#/components/schemas/GameState' + + ErrorEvent: + type: object + description: Emitted on a game stream when an error occurs. + required: [type, error] + properties: + type: + type: string + enum: [error] + error: + $ref: '#/components/schemas/ApiError' + + # ------------------------------------------------------------------------- + # Shared types + # ------------------------------------------------------------------------- + + PlayerInfo: + type: object + required: [id, displayName] + properties: + id: + type: string + description: Unique player identifier + example: player1 + displayName: + type: string + description: Human-readable display name + example: Alice + + OkResponse: + type: object + required: [ok] + properties: + ok: + type: boolean + enum: [true] + + ApiError: + type: object + required: [code, message] + properties: + code: + type: string + description: Machine-readable error code + example: INVALID_MOVE + message: + type: string + description: Human-readable error description + example: e2e5 is not a legal move + field: + type: string + description: Request field that caused the error, if applicable + example: uci + nullable: true