Merge branch 'chore/49-serverside-documentation-for-handling-request' into 'main'

Write documentation on how to implement additional commands on the server

Closes #49

See merge request cs108-fs26/Gruppe-13!76
This commit was merged in pull request #232.
This commit is contained in:
Lars Simon Winzer
2026-04-04 18:14:06 +02:00
21 changed files with 501 additions and 0 deletions
@@ -0,0 +1,31 @@
# Commands
Commands are the primary extension point of the server.
Every client-facing operation e.g. checking a username, sending a chat message, joining a lobby is implemented as a command.
Each command consists of four classes: a **Parser**, a **Request**, a **Handler**, and a **Response**.
The infrastructure for routing and dispatching commands lives in the `network/` layer and is intentionally kept generic.
The concrete implementations for each command live in `app/commands/` and are wired together at startup in `ServerApp`.
![Overview of all components directly related to executing requests](/documents/images/docs/networking/commands/overview.svg)
## Contents
### Guides
- [Implementing a Command](./guide-on-implementing-a-command.md) -
Step-by-step walkthrough for adding a new command to the server, including registration and common pitfalls.
### Reference
- [Command Infrastructure](./commands-deep-dive.md) -
Technical deep-dive into the `CommandParser`, `CommandParserDispatcher`, `CommandHandler`, `CommandRouter`, `Request`, and the response hierarchy.
## Key Concepts
**Each command is self-contained.**
A command's Parser, Request, Handler, and Response all live in the same package under `app/commands/<name>/`.
This keeps related code co-located and makes it easy to reason about a single command without navigating across multiple directories.
**The `network/` layer knows nothing about specific commands.**
`CommandParser` and `CommandHandler` are generic interfaces. The `CommandParserDispatcher` and `CommandRouter` operate on those interfaces.
Adding a new command **never** requires modifying infrastructure code.
**Registration happens at the composition root.**
All commands are wired in `ServerApp` by calling `parserDispatcher.register(...)` and `commandRouter.register(...)`.
This keeps the wiring explicit and compiler-checked.
@@ -0,0 +1,108 @@
# Command Infrastructure
This document describes the generic command infrastructure that lives in the `network/` layer.
It covers the parsing pipeline, the routing pipeline, and the base types that every command builds on.
The infrastructure is intentionally project-agnostic: It has no knowledge of specific commands and is never modified when a new command is added.
## Overview
An incoming request travels through two sequential pipelines: **parsing** and **execution**.
The parsing pipeline converts a stringly-typed `PrimitiveRequest` into a strongly-typed `Request` subclass.
The execution pipeline routes that typed request to the correct handler, which produces a `Response`.
![Sequence diagram of all involved components to process and respond to an incoming request](/documents/images/docs/networking/commands/sequence_diagram.svg)
## Parsing Pipeline
### `CommandParser<T extends Request>`
![Class diagram of the CommandParser](/documents/images/docs/networking/commands/command_parser.svg)
The `CommandParser` is a single-method interface responsible for converting a `PrimitiveRequest` into a concrete, typed `Request` subclass.
Implementations live in `app/commands/<name>/` and are registered by name in `CommandParserDispatcher`.
The parser is the correct place to validate and extract parameters.
If a required parameter is absent, `RequestParameterAccessor.require(...)` throws an `MissingParameterException`, which the `SessionReader` catches and converts into a `MISSING_PARAMETER` error response for the client.
Parsers **do not** perform any domain logic. Their only job is extraction and type conversion.
### `CommandParserDispatcher`
![Class diagram of CommandParserDispatcher](/documents/images/docs/networking/commands/command_parser_dispatcher.svg)
The `CommandParserDispatcher` holds a map from command name strings (e.g. `"PING"`) to their corresponding `CommandParser`.
When the `SessionReader` receives a `PrimitiveRequest`, it calls `dispatcher.parse(...)`, which looks up the parser by the request's command string and delegates parsing.
If no parser is registered for the command name, `parse(...)` throws an `UnknownCommandException`, which `SessionReader` catches and converts into an `UNKNOWN_COMMAND` error response for the client.
### `RequestParameterAccessor`
![Class diagram of RequestParameterAccessor](/documents/images/docs/networking/commands/request_parameter_accessor.svg)
The `RequestParameterAccessor` is a helper provided to parsers for reading typed parameter values from a `PrimitiveRequest`.
It indexes the parameter list by key on construction for O(1) lookups.
```java
// Require a parameter — throws MissingParameterException if absent
String username = accessor.require("USERNAME");
// Require and parse — throws ParameterParseException if conversion fails
int count = accessor.require("COUNT", Integer::parseInt);
// Optional with a default
String mode = accessor.optional("MODE", "default");
```
The `ThrowingParser<T>` functional interface accepted by the typed overloads allows any checked or unchecked exception to propagate from the conversion function.
The `RequestParameterAccessor` wraps it in a `ParameterParseException`.
## Execution Pipeline
### `Request`
![Class diagram of Request](/documents/images/docs/networking/commands/request.svg)
The `Request` is the abstract base class for all typed command requests. It carries a `RequestContext`, an immutable record containing the originating `SessionId` and the numeric `requestId`.
Both of which are later used by the handler to direct the response to the correct session.
Concrete subclasses add command-specific fields, all set via constructor. Requests are immutable value objects. They carry data, not behaviour.
### `CommandHandler<T extends Request>`
![Class diagram of CommandHandler](/documents/images/docs/networking/commands/command_handler.svg)
The `CommandHandler` is a single-method interface responsible for executing a typed request.
The handler contains the domain logic: reading from registries and managers, modifying state, and dispatching a response via the `ResponseDispatcher`.
Handlers receive their dependencies (the `ResponseDispatcher`, registries and managers etc.) through constructor injection.
This keeps them fully testable without the network layer.
### `CommandRouter`
![Class diagram of CommandRouter](/documents/images/docs/networking/commands/command_router.svg)
The `CommandRouter` maps `Request` subclasses to their handlers using the request's runtime class as the key.
The type safety of `register(...)` ensures that a handler can only be registered for the exact type it is parameterised on.
The unchecked cast in `execute(...)` is therefore safe by construction and is documented with a `@SuppressWarnings` comment in the source.
If no handler is registered for the given request type, `execute(...)` throws `UnknownRequestException`.
Unlike `UnknownCommandException` (which covers unknown command strings), this exception indicates a programming error i.e. a parser was registered without a corresponding handler.
## Response Types
![Class diagram of the response interface and built-in implementations](/documents/images/docs/networking/commands/response_types.svg)
The `Response` is the abstract base for all server responses. Its two concrete branches are `SuccessResponse` (prefix `+OK`) and `ErrorResponse` (prefix `-ERR`).
Command-specific responses extend `SuccessResponse` and populate the body using the `ResponseBodyBuilder`.
`OkResponse` is a pre-built convenience subclass of `SuccessResponse` with an empty body, used for commands that need only acknowledge success without returning data (e.g. `PING`).
The body is built with a fluent `ResponseBodyBuilder`:
```java
// Simple key/value parameters
new ResponseBodyBuilder()
.param("STATUS", UsernameAvailability.FREE)
.build();
// Nested block
new ResponseBodyBuilder()
.block("USER", b -> b
.param("ID", user.getId().value())
.param("NAME", user.getName()))
.build();
```
`ResponseEncoder` serialises the body into the wire format (tab-indented, `END`-terminated blocks) and wraps it in a `PrimitiveResponse`.
`ResponseDispatcher` then enqueues this into the target session's bounded response queue.
@@ -0,0 +1,215 @@
# Implementing a Command
This guide walks through the full process of adding a new command to the server. By the end you
will have a working command with a Parser, Request, Handler, and Response, all correctly wired
into the server.
For background on how these components interact at a technical level, see [Command Infrastructure](../reference/command-infrastructure.md).
## Before You Start
A command consists of exactly four classes, all placed in the same package:
```
app/commands/<your_command>/
YourCommandParser.java
YourCommandRequest.java
YourCommandHandler.java
YourCommandResponse.java ← omit this if OkResponse is sufficient
```
Name the package after the command in `snake_case`, matching the wire-protocol name (e.g. `join_lobby` for the `JOIN_LOBBY` command).
Name the classes in `UpperCamelCase` with the command name as prefix.
## Step 1 — Define the Request
The `Request` subclass is a typed, immutable value object holding everything the handler needs.
Define it first, because both the Parser and the Handler depend on it.
```java
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.greet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
public class GreetRequest extends Request {
private final String name;
public GreetRequest(RequestContext context, String name) {
super(context);
this.name = name;
}
public String getName() {
return name;
}
}
```
**Rules for the Request class:**
- Always pass `context` directly to `super(context)`. Never store it in a separate field.
- Fields must be `private final`. Set them only through the constructor.
- Provide a getter for every field. No setters.
- No logic. The request is data, not behaviour.
## Step 2 — Implement the Parser
The Parser extracts parameters from the `PrimitiveRequest` and constructs the typed Request.
```java
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.greet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
public class GreetParser implements CommandParser<GreetRequest> {
@Override
public GreetRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor = new RequestParameterAccessor(primitiveRequest.parameters());
String name = accessor.require("NAME");
return new GreetRequest(primitiveRequest.context(), name);
}
}
```
**Rules for the Parser class:**
- Always create a `RequestParameterAccessor` from `primitiveRequest.parameters()`.
- Use `accessor.require(key)` for mandatory parameters. It throws `MissingParameterException`
automatically — do not write your own null checks.
- Use `accessor.optional(key, defaultValue)` for optional parameters.
- Use the typed overloads (e.g. `accessor.require("COUNT", Integer::parseInt)`) for non-string
parameters. The resulting `ParameterParseException` is handled by `SessionReader`.
- Always pass `primitiveRequest.context()` as the first argument to the Request constructor.
- No domain logic.
### Choosing between `require` and `optional`
| Use | When |
|-|-|
| `accessor.require(key)` | The command cannot function without this parameter |
| `accessor.require(key, parser)` | Same, but the value must be converted to a specific type |
| `accessor.optional(key, default)` | The parameter has a sensible default when omitted |
| `accessor.optional(key, default, parser)` | Optional + type conversion |
## Step 3 — Implement the Response
If your command returns data, create a dedicated Response class. If it only needs to signal
success, use `OkResponse` directly in the handler and skip this step.
```java
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.greet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
public class GreetResponse extends SuccessResponse {
public GreetResponse(RequestContext context, String greeting) {
super(context, new ResponseBodyBuilder()
.param("GREETING", greeting)
.build());
}
}
```
**Rules for the Response class:**
- Extend `SuccessResponse` for successful outcomes, not `Response` directly.
- Build the body inline in the `super(...)` call using `ResponseBodyBuilder`. Do not store
the builder or body separately.
- Use `ResponseBodyBuilder.block(tag, consumer)` to add nested structures when the response
carries a list or a complex sub-object.
- Parameter keys must be `UPPER_SNAKE_CASE` to match the wire protocol convention.
- If you need to return an error (e.g. lobby not found), do not throw — dispatch an
`ErrorResponse` from the handler instead (see Step 4).
## Step 4 — Implement the Handler
The Handler contains the domain logic. It reads from registries, modifies state if needed, and
always dispatches exactly one response.
```java
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.greet;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
public class GreetHandler implements CommandHandler<GreetRequest> {
public final ResponseDispatcher responseDispatcher;
public GreetHandler(ResponseDispatcher responseDispatcher) {
this.responseDispatcher = responseDispatcher;
}
@Override
public void execute(GreetRequest request) {
GreetResponse response = new GreetResponse(request.getContext(), "Hello " + request.getName() + ", nice to meet you");
responseDispatcher.dispatch(response);
}
}
```
**Rules for the Handler class:**
- Declare all dependencies as `private final` fields, injected through the constructor.
- Always dispatch exactly one response per execution path. Every branch must end with a
`responseDispatcher.dispatch(...)` call.
- Use `ErrorResponse` for domain-level failures (e.g. entity not found, precondition not met).
Do not throw exceptions for expected failure cases.
- Use `request.getContext()` when constructing any Response — never construct a `RequestContext`
yourself.
- Do not call `responseDispatcher.dispatch(...)` more than once in a single `execute` invocation.
## Step 5 — Register the Command
Open `ServerApp.registerCommands(...)` and add two lines: one to register the parser and one to
register the handler.
```java
private static void registerCommands(
CommandParserDispatcher parserDispatcher,
CommandRouter commandRouter,
ResponseDispatcher responseDispatcher /* add new dependencies here */) {
// ... existing commands ...
parserDispatcher.register("GREET", new GreetParser());
commandRouter.register(GreetRequest.class,
new GreetHandler(responseDispatcher));
}
```
The string passed to `parserDispatcher.register(...)` must exactly match the command name as
sent by the client on the wire, in `UPPER_SNAKE_CASE`.
> **Important:** Always register both the parser **and** the handler. Registering a parser
> without a handler will result in an `UnknownRequestException` at runtime when the command is
> received — the parser will succeed, but the router will find no handler for the resulting
> request type.
## Common Mistakes
**Forgetting to pass `context` through to the Response.** The `context` is how the response
finds its way back to the right client. Dropping it means the response is dispatched to the
wrong session or causes a NullPointerException.
**Putting domain logic in the Parser.** Parsers run before the request is validated as
meaningful. A parser that calls a registry or modifies state creates hidden coupling between the
parsing and execution phases and makes the parser difficult to test.
**Dispatching a response before an early return.** A common mistake is to dispatch an error
and then fall through to dispatch a success response as well. Always `return` immediately after
dispatching an error.
**Using a raw string for the error code.** Error codes should be `UPPER_SNAKE_CASE` constant
strings that the client can match against programmatically. Avoid spaces or punctuation.
@@ -0,0 +1,8 @@
@startuml
skinparam backgroundColor transparent
interface CommandHandler<T extends Request> {
+ execute(request: T): void
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="85px" preserveAspectRatio="none" style="width:287px;height:85px;" version="1.1" viewBox="0 0 287 85" width="287px" zoomAndPan="magnify"><defs/><g><!--class CommandHandler--><g class="entity" data-qualified-name="CommandHandler" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="64.4883" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="266.835" x="7" y="7"/><ellipse cx="22" cy="23" fill="#B4A7E5" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M18.4277,19.2651 L18.4277,17.1069 L25.8071,17.1069 L25.8071,19.2651 L23.3418,19.2651 L23.3418,27.3418 L25.8071,27.3418 L25.8071,29.5 L18.4277,29.5 L18.4277,27.3418 L20.8931,27.3418 L20.8931,19.2651 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" font-style="italic" lengthAdjust="spacing" textLength="122.7529" x="36" y="28.291">CommandHandler</text><rect fill="#FFFFFF" height="16.1328" style="stroke:#181818;stroke-width:1;stroke-dasharray:2,2;" width="110.082" x="166.7529" y="4"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="108.082" x="167.7529" y="16.6016">T extends Request</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="272.835" y1="39" y2="39"/><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="272.835" y1="47" y2="47"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="60.7441" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="168.6631" x="27" y="64.5352">execute(request: T): void</text></g><?plantuml-src 9Sb12W8n34RXVKwHfU8D1d4p6zSo5uZjhvIP9fgc8eXtj-23j_hQMlGXBfMlajQxMzSyssREuQ9j43I8YWRy9WayMwsY-JpUuCU5yGveRdp1iwF5YJ_4eyC0f1xO-HycNlOJlwna-j8F?></g></svg>
@@ -0,0 +1,8 @@
@startuml
skinparam backgroundColor transparent
interface CommandParser<T extends Request> {
+ parse(primitiveRequest: PrimitiveRequest): T
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="85px" preserveAspectRatio="none" style="width:347px;height:85px;" version="1.1" viewBox="0 0 347 85" width="347px" zoomAndPan="magnify"><defs/><g><!--class CommandParser--><g class="entity" data-qualified-name="CommandParser" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="64.4883" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="326.5283" x="7" y="7"/><ellipse cx="54.2124" cy="23" fill="#B4A7E5" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M50.6401,19.2651 L50.6401,17.1069 L58.0195,17.1069 L58.0195,19.2651 L55.5542,19.2651 L55.5542,27.3418 L58.0195,27.3418 L58.0195,29.5 L50.6401,29.5 L50.6401,27.3418 L53.1055,27.3418 L53.1055,19.2651 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" font-style="italic" lengthAdjust="spacing" textLength="111.5215" x="74.7124" y="28.291">CommandParser</text><rect fill="#FFFFFF" height="16.1328" style="stroke:#181818;stroke-width:1;stroke-dasharray:2,2;" width="110.082" x="226.4463" y="4"/><text fill="#000000" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="108.082" x="227.4463" y="16.6016">T extends Request</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="332.5283" y1="39" y2="39"/><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="332.5283" y1="47" y2="47"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="60.7441" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="300.5283" x="27" y="64.5352">parse(primitiveRequest: PrimitiveRequest): T</text></g><?plantuml-src NOf12i9034NtESLVAjwWY9jUe4Wl43jHXZgPCKcB8DxT2gvyo_UUpraR6nTSU9flLXTDVRrNGnYhRqaqYBA6s9KdGLzBOKq3cuiTHyWpH9FZB8z5F4vu4JOEQDzZrooN77cLN-ym_9bzXv7Uz040?></g></svg>
@@ -0,0 +1,11 @@
@startuml
skinparam backgroundColor transparent
class CommandParserDispatcher {
- parsers: Map<String, CommandParser>
+ register(command: String, parser: CommandParser): void
+ parse(primitiveRequest: PrimitiveRequest): Request
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="118px" preserveAspectRatio="none" style="width:434px;height:118px;" version="1.1" viewBox="0 0 434 118" width="434px" zoomAndPan="magnify"><defs/><g><!--class CommandParserDispatcher--><g class="entity" data-qualified-name="CommandParserDispatcher" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="97.4648" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="413.0234" x="7" y="7"/><ellipse cx="117.0825" cy="23" fill="#ADD1B2" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M119.5557,29.1431 Q118.9746,29.4419 118.3354,29.5913 Q117.6963,29.7407 116.9907,29.7407 Q114.4839,29.7407 113.1641,28.0889 Q111.8442,26.437 111.8442,23.3159 Q111.8442,20.1865 113.1641,18.5347 Q114.4839,16.8828 116.9907,16.8828 Q117.6963,16.8828 118.3438,17.0322 Q118.9912,17.1816 119.5557,17.4805 L119.5557,20.2031 Q118.9248,19.6221 118.3313,19.3523 Q117.7378,19.0825 117.1069,19.0825 Q115.7622,19.0825 115.0774,20.1492 Q114.3926,21.2158 114.3926,23.3159 Q114.3926,25.4077 115.0774,26.4744 Q115.7622,27.541 117.1069,27.541 Q117.7378,27.541 118.3313,27.2712 Q118.9248,27.0015 119.5557,26.4204 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="184.3584" x="137.5825" y="28.291">CommandParserDispatcher</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="419.0234" y1="39" y2="39"/><g data-visibility-modifier="PRIVATE_FIELD"><rect fill="none" height="6" style="stroke:#C82930;stroke-width:1;" width="6" x="15" y="49.7441"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="270.1426" x="27" y="56.5352">parsers: Map&lt;String, CommandParser&gt;</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="419.0234" y1="63.4883" y2="63.4883"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="77.2324" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="387.0234" x="27" y="81.0234">register(command: String, parser: CommandParser): void</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="93.7207" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="346.001" x="27" y="97.5117">parse(primitiveRequest: PrimitiveRequest): Request</text></g><?plantuml-src NOmx3i8m343tdC9Z4N212B6K5Qc24vZKAbQR3tRQ1N5tehOCnPFbzvwrvv1GqCC3NTz974DRno4APC6W1L78nhW1LQ6EtcDe6nGbkN2XsJr9u6sWp17I3DJ25TFfdeL3TzX6PpEhUn3gM3D9vHPiuUSlJ-ossrcO8hThF2jL4lQSUQ8RlKRIRA7vkvHgtSp7V040?></g></svg>
@@ -0,0 +1,10 @@
@startuml
skinparam backgroundColor transparent
class CommandRouter {
- handlers: Map<Class<? extends Request>, CommandHandler<?>>
+ register(requestClass: Class<T>, handler: CommandHandler<T>): void
+ execute(request: Request): void
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="118px" preserveAspectRatio="none" style="width:531px;height:118px;" version="1.1" viewBox="0 0 531 118" width="531px" zoomAndPan="magnify"><defs/><g><!--class CommandRouter--><g class="entity" data-qualified-name="CommandRouter" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="97.4648" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="510.5996" x="7" y="7"/><ellipse cx="200.7681" cy="23" fill="#ADD1B2" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M203.2412,29.1431 Q202.6602,29.4419 202.021,29.5913 Q201.3818,29.7407 200.6763,29.7407 Q198.1694,29.7407 196.8496,28.0889 Q195.5298,26.437 195.5298,23.3159 Q195.5298,20.1865 196.8496,18.5347 Q198.1694,16.8828 200.6763,16.8828 Q201.3818,16.8828 202.0293,17.0322 Q202.6768,17.1816 203.2412,17.4805 L203.2412,20.2031 Q202.6104,19.6221 202.0168,19.3523 Q201.4233,19.0825 200.7925,19.0825 Q199.4478,19.0825 198.7629,20.1492 Q198.0781,21.2158 198.0781,23.3159 Q198.0781,25.4077 198.7629,26.4744 Q199.4478,27.541 200.7925,27.541 Q201.4233,27.541 202.0168,27.2712 Q202.6104,27.0015 203.2412,26.4204 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="114.5635" x="221.2681" y="28.291">CommandRouter</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="516.5996" y1="39" y2="39"/><g data-visibility-modifier="PRIVATE_FIELD"><rect fill="none" height="6" style="stroke:#C82930;stroke-width:1;" width="6" x="15" y="49.7441"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="459.2725" x="27" y="56.5352">handlers: Map&lt;Class&lt;? extends Request&gt;, CommandHandler&lt;?&gt;&gt;</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="516.5996" y1="63.4883" y2="63.4883"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="77.2324" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="484.5996" x="27" y="81.0234">register(requestClass: Class&lt;T&gt;, handler: CommandHandler&lt;T&gt;): void</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="93.7207" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="214.1357" x="27" y="97.5117">execute(request: Request): void</text></g><?plantuml-src NOun3i8m34NtdC8Z2BX0LAL35nQMgXSmZLMgjacn4rG9SNT2sonujDz_tpBqRaR62UxO3Xtxw6pbHyyG69sa4xcWL3kY25H-cj3PsiT036y5QIxmIAkHM3JSS2wg7rcKG4iWPmLgUaQIOCuvVTtmeZHc5Po0gUib6G-yiQj2msPgKd9lqF-AnXmrl7nlTn4jrARNiaVdyanzr1S0?></g></svg>
@@ -0,0 +1,33 @@
@startuml
skinparam backgroundColor transparent
package "network/ (infrastructure)" {
class PrimitiveRequest <<record>>
interface CommandParser<T extends Request>
interface CommandHandler<T extends Request>
class CommandParserDispatcher
class CommandRouter
abstract class Request
abstract class Response
PrimitiveRequest --> CommandParserDispatcher : routes
PrimitiveRequest ..> CommandParser : parsed by
CommandParserDispatcher --> CommandParser : routes to
CommandParser --> Request : parses to
Request --> CommandRouter : routes
CommandRouter --> CommandHandler : routes to
Request ..> CommandHandler : executed by
CommandHandler --> Response : creates
}
package "app/commands/<name>/ (per command)" {
class ExampleParser implements CommandParser
class ExampleRequest extends Request
class ExampleHandler implements CommandHandler
class ExampleResponse extends Response
ExampleParser --> ExampleRequest : parses to
ExampleRequest --> ExampleHandler : executed by
ExampleHandler --> ExampleResponse : produces
}
@enduml
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
@startuml
skinparam backgroundColor transparent
abstract class Request {
# context: RequestContext
+ getContext(): RequestContext
+ getSessionId(): SessionId
+ getRequestId(): int
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="134px" preserveAspectRatio="none" style="width:248px;height:134px;" version="1.1" viewBox="0 0 248 134" width="248px" zoomAndPan="magnify"><defs/><g><!--class Request--><g class="entity" data-qualified-name="Request" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="113.9531" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="227.7217" x="7" y="7"/><ellipse cx="89.4482" cy="23" fill="#A9DCDF" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M89.3115,18.3481 L88.1577,23.4199 L90.4736,23.4199 Z M87.8174,16.1069 L90.814,16.1069 L94.1592,28.5 L91.7104,28.5 L90.9468,25.437 L87.668,25.437 L86.9209,28.5 L84.4722,28.5 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" font-style="italic" lengthAdjust="spacing" textLength="54.3252" x="109.9482" y="28.291">Request</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="233.7217" y1="39" y2="39"/><g data-visibility-modifier="PROTECTED_FIELD"><polygon fill="none" points="18,47.7441,22,51.7441,18,55.7441,14,51.7441" style="stroke:#B38D22;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="168.335" x="27" y="56.5352">context: RequestContext</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="233.7217" y1="63.4883" y2="63.4883"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="77.2324" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="201.7217" x="27" y="81.0234">getContext(): RequestContext</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="93.7207" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="167.3164" x="27" y="97.5117">getSessionId(): SessionId</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="110.209" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="124.8721" x="27" y="114">getRequestId(): int</text></g><?plantuml-src AyxEp2j8B4hCLKXAJCvEByelpKjnpi_9Br8eAKhCAmPAfUQLS74b9XK3-Sab2iavYSN52a6fXQMfnIKArLmAGA2ia9oVLv9QKM85at26yC6osWhfgJ013KtSIe9JYuipy_CyKq2Au1o4F5GVH3uJwAXQBW00?></g></svg>
@@ -0,0 +1,14 @@
@startuml
skinparam backgroundColor transparent
class RequestParameterAccessor {
- index: Map<String, String>
+ RequestParameterAccessor(parameters: List<RequestParameters>)
+ require(key: String): String
+ require(key: String, parser: ThrowingParser<T>): T
+ optional(key: String, defaultValue: String): String
+ optional(key: String, defaultValue: T, parser: ThrowingParser<T>): T
}
@enduml
@@ -0,0 +1 @@
<?plantuml 1.2026.2?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" data-diagram-type="CLASS" height="167px" preserveAspectRatio="none" style="width:510px;height:167px;" version="1.1" viewBox="0 0 510 167" width="510px" zoomAndPan="magnify"><defs/><g><!--class RequestParameterAccessor--><g class="entity" data-qualified-name="RequestParameterAccessor" data-source-line="3" id="ent0002"><rect fill="#F1F1F1" height="146.9297" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="489.9072" x="7" y="7"/><ellipse cx="156.0576" cy="23" fill="#ADD1B2" rx="11" ry="11" style="stroke:#181818;stroke-width:1;"/><path d="M158.5308,29.1431 Q157.9497,29.4419 157.3105,29.5913 Q156.6714,29.7407 155.9658,29.7407 Q153.459,29.7407 152.1392,28.0889 Q150.8193,26.437 150.8193,23.3159 Q150.8193,20.1865 152.1392,18.5347 Q153.459,16.8828 155.9658,16.8828 Q156.6714,16.8828 157.3188,17.0322 Q157.9663,17.1816 158.5308,17.4805 L158.5308,20.2031 Q157.8999,19.6221 157.3064,19.3523 Q156.7129,19.0825 156.082,19.0825 Q154.7373,19.0825 154.0525,20.1492 Q153.3677,21.2158 153.3677,23.3159 Q153.3677,25.4077 154.0525,26.4744 Q154.7373,27.541 156.082,27.541 Q156.7129,27.541 157.3064,27.2712 Q157.8999,27.0015 158.5308,26.4204 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="183.292" x="176.5576" y="28.291">RequestParameterAccessor</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="495.9072" y1="39" y2="39"/><g data-visibility-modifier="PRIVATE_FIELD"><rect fill="none" height="6" style="stroke:#C82930;stroke-width:1;" width="6" x="15" y="49.7441"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="186.4502" x="27" y="56.5352">index: Map&lt;String, String&gt;</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="495.9072" y1="63.4883" y2="63.4883"/><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="77.2324" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="454.2002" x="27" y="81.0234">RequestParameterAccessor(parameters: List&lt;RequestParameters&gt;)</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="93.7207" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="178.6641" x="27" y="97.5117">require(key: String): String</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="110.209" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="345.2969" x="27" y="114">require(key: String, parser: ThrowingParser&lt;T&gt;): T</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="126.6973" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="328.3916" x="27" y="130.4883">optional(key: String, defaultValue: String): String</text><g data-visibility-modifier="PUBLIC_METHOD"><ellipse cx="18" cy="143.1855" fill="#84BE84" rx="3" ry="3" style="stroke:#038048;stroke-width:1;"/></g><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="463.9072" x="27" y="146.9766">optional(key: String, defaultValue: T, parser: ThrowingParser&lt;T&gt;): T</text></g><?plantuml-src ZOvD2i9038NtSueiMzGBb58Gjmf5Y_lOHXqwpjHaYefuTyVQBXIAPfE_xxr4QcqRP3p13ilwmAwrrS8Pn-0PhOGLMQzKQL04rdHkINp-uiaJpykIH09xLn1Y1jfMT4rWXKswyQpjOGhldAcEc8nQHCqmaGIMMdpwJKeMznuSDfefgkcMzFxnK8mZKmWdX3Y1uiZk4YPvrwT55jH5BtIDrywY-LHMjCVM-2sQbiPE_gCiVZtnK4y0?></g></svg>
@@ -0,0 +1,23 @@
@startuml
skinparam backgroundColor transparent
abstract class Response {
# context: RequestContext
# body: ResponseBody
+ prefix(): String
+ getSessionId(): SessionId
+ getRequestId(): int
+ getBody(): ResponseBody
}
abstract class SuccessResponse extends Response {
+ prefix(): String (+OK)
}
class OkResponse extends SuccessResponse
class ErrorResponse extends Response {
+ prefix(): String (-ERR)
}
@enduml
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
@startuml
skinparam backgroundColor transparent
participant SessionReader
participant CommandParserDispatcher as Dispatcher
participant "CommandParser<T>" as Parser
participant CommandRouter as Router
participant "CommandHandler<T>" as Handler
participant ResponseDispatcher
SessionReader -> Dispatcher : parse(primitiveRequest)
Dispatcher -> Parser : parse(primitiveRequest)
Parser --> Dispatcher : ExampleRequest
Dispatcher --> SessionReader : Request
SessionReader -> Router : execute(request)
Router -> Handler : execute(ExampleRequest)
Handler -> ResponseDispatcher : dispatch(ExampleRequest)
@enduml
File diff suppressed because one or more lines are too long