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:
@@ -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`.
|
||||
|
||||

|
||||
|
||||
## 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`.
|
||||
|
||||

|
||||
|
||||
## Parsing Pipeline
|
||||
### `CommandParser<T extends Request>`
|
||||

|
||||
|
||||
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`
|
||||

|
||||
|
||||
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`
|
||||

|
||||
|
||||
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`
|
||||

|
||||
|
||||
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>`
|
||||

|
||||
|
||||
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`
|
||||

|
||||
|
||||
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
|
||||

|
||||
|
||||
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<String, CommandParser></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<Class<? extends Request>, CommandHandler<?>></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<T>, handler: CommandHandler<T>): 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<String, String></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<RequestParameters>)</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<T>): 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<T>): 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
Reference in New Issue
Block a user