From f971e6ad5bb72697e7de14abf3c01402363b288c Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sat, 4 Apr 2026 15:51:29 +0200 Subject: [PATCH] Docs: Write technical deep dive for commands and link to it from README Includes puml diagrams and exported svgs --- documents/docs/networking/commands/README.md | 3 +- .../networking/commands/commands-deep-dive.md | 108 ++++++++++++++++++ .../networking/commands/command_handler.puml | 8 ++ .../networking/commands/command_handler.svg | 1 + .../networking/commands/command_parser.puml | 8 ++ .../networking/commands/command_parser.svg | 1 + .../commands/command_parser_dispatcher.puml | 11 ++ .../commands/command_parser_dispatcher.svg | 1 + .../networking/commands/command_router.puml | 10 ++ .../networking/commands/command_router.svg | 1 + .../docs/networking/commands/overview.puml | 2 +- .../docs/networking/commands/request.puml | 11 ++ .../docs/networking/commands/request.svg | 1 + .../commands/request_parameter_accessor.puml | 14 +++ .../commands/request_parameter_accessor.svg | 1 + .../networking/commands/response_types.puml | 23 ++++ .../networking/commands/response_types.svg | 1 + .../networking/commands/sequence_diagram.puml | 20 ++++ .../networking/commands/sequence_diagram.svg | 1 + 19 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 documents/docs/networking/commands/commands-deep-dive.md create mode 100644 documents/images/docs/networking/commands/command_handler.puml create mode 100644 documents/images/docs/networking/commands/command_handler.svg create mode 100644 documents/images/docs/networking/commands/command_parser.puml create mode 100644 documents/images/docs/networking/commands/command_parser.svg create mode 100644 documents/images/docs/networking/commands/command_parser_dispatcher.puml create mode 100644 documents/images/docs/networking/commands/command_parser_dispatcher.svg create mode 100644 documents/images/docs/networking/commands/command_router.puml create mode 100644 documents/images/docs/networking/commands/command_router.svg create mode 100644 documents/images/docs/networking/commands/request.puml create mode 100644 documents/images/docs/networking/commands/request.svg create mode 100644 documents/images/docs/networking/commands/request_parameter_accessor.puml create mode 100644 documents/images/docs/networking/commands/request_parameter_accessor.svg create mode 100644 documents/images/docs/networking/commands/response_types.puml create mode 100644 documents/images/docs/networking/commands/response_types.svg create mode 100644 documents/images/docs/networking/commands/sequence_diagram.puml create mode 100644 documents/images/docs/networking/commands/sequence_diagram.svg diff --git a/documents/docs/networking/commands/README.md b/documents/docs/networking/commands/README.md index a7401b0..aa0d5a9 100644 --- a/documents/docs/networking/commands/README.md +++ b/documents/docs/networking/commands/README.md @@ -13,7 +13,8 @@ The concrete implementations for each command live in `app/commands/` and are wi *Link to guides* ### Reference -*Link to technical deep dives* +- [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.** diff --git a/documents/docs/networking/commands/commands-deep-dive.md b/documents/docs/networking/commands/commands-deep-dive.md new file mode 100644 index 0000000..3999384 --- /dev/null +++ b/documents/docs/networking/commands/commands-deep-dive.md @@ -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` +![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//` 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` 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` +![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. diff --git a/documents/images/docs/networking/commands/command_handler.puml b/documents/images/docs/networking/commands/command_handler.puml new file mode 100644 index 0000000..35337aa --- /dev/null +++ b/documents/images/docs/networking/commands/command_handler.puml @@ -0,0 +1,8 @@ +@startuml +skinparam backgroundColor transparent + +interface CommandHandler { + + execute(request: T): void +} + +@enduml diff --git a/documents/images/docs/networking/commands/command_handler.svg b/documents/images/docs/networking/commands/command_handler.svg new file mode 100644 index 0000000..750344e --- /dev/null +++ b/documents/images/docs/networking/commands/command_handler.svg @@ -0,0 +1 @@ +CommandHandlerT extends Requestexecute(request: T): void \ No newline at end of file diff --git a/documents/images/docs/networking/commands/command_parser.puml b/documents/images/docs/networking/commands/command_parser.puml new file mode 100644 index 0000000..80df3aa --- /dev/null +++ b/documents/images/docs/networking/commands/command_parser.puml @@ -0,0 +1,8 @@ +@startuml +skinparam backgroundColor transparent + +interface CommandParser { + + parse(primitiveRequest: PrimitiveRequest): T +} + +@enduml diff --git a/documents/images/docs/networking/commands/command_parser.svg b/documents/images/docs/networking/commands/command_parser.svg new file mode 100644 index 0000000..357abf4 --- /dev/null +++ b/documents/images/docs/networking/commands/command_parser.svg @@ -0,0 +1 @@ +CommandParserT extends Requestparse(primitiveRequest: PrimitiveRequest): T \ No newline at end of file diff --git a/documents/images/docs/networking/commands/command_parser_dispatcher.puml b/documents/images/docs/networking/commands/command_parser_dispatcher.puml new file mode 100644 index 0000000..6079366 --- /dev/null +++ b/documents/images/docs/networking/commands/command_parser_dispatcher.puml @@ -0,0 +1,11 @@ +@startuml +skinparam backgroundColor transparent + +class CommandParserDispatcher { + - parsers: Map + + + register(command: String, parser: CommandParser): void + + parse(primitiveRequest: PrimitiveRequest): Request +} + +@enduml diff --git a/documents/images/docs/networking/commands/command_parser_dispatcher.svg b/documents/images/docs/networking/commands/command_parser_dispatcher.svg new file mode 100644 index 0000000..7f61e6e --- /dev/null +++ b/documents/images/docs/networking/commands/command_parser_dispatcher.svg @@ -0,0 +1 @@ +CommandParserDispatcherparsers: Map<String, CommandParser>register(command: String, parser: CommandParser): voidparse(primitiveRequest: PrimitiveRequest): Request \ No newline at end of file diff --git a/documents/images/docs/networking/commands/command_router.puml b/documents/images/docs/networking/commands/command_router.puml new file mode 100644 index 0000000..210444f --- /dev/null +++ b/documents/images/docs/networking/commands/command_router.puml @@ -0,0 +1,10 @@ +@startuml +skinparam backgroundColor transparent + +class CommandRouter { + - handlers: Map, CommandHandler> + + register(requestClass: Class, handler: CommandHandler): void + + execute(request: Request): void +} + +@enduml diff --git a/documents/images/docs/networking/commands/command_router.svg b/documents/images/docs/networking/commands/command_router.svg new file mode 100644 index 0000000..7dc0117 --- /dev/null +++ b/documents/images/docs/networking/commands/command_router.svg @@ -0,0 +1 @@ +CommandRouterhandlers: Map<Class<? extends Request>, CommandHandler<?>>register(requestClass: Class<T>, handler: CommandHandler<T>): voidexecute(request: Request): void \ No newline at end of file diff --git a/documents/images/docs/networking/commands/overview.puml b/documents/images/docs/networking/commands/overview.puml index 112f52e..da85ed7 100644 --- a/documents/images/docs/networking/commands/overview.puml +++ b/documents/images/docs/networking/commands/overview.puml @@ -30,4 +30,4 @@ package "app/commands// (per command)" { ExampleRequest --> ExampleHandler : executed by ExampleHandler --> ExampleResponse : produces } -@enduml \ No newline at end of file +@enduml diff --git a/documents/images/docs/networking/commands/request.puml b/documents/images/docs/networking/commands/request.puml new file mode 100644 index 0000000..9e27697 --- /dev/null +++ b/documents/images/docs/networking/commands/request.puml @@ -0,0 +1,11 @@ +@startuml +skinparam backgroundColor transparent + +abstract class Request { + # context: RequestContext + + getContext(): RequestContext + + getSessionId(): SessionId + + getRequestId(): int +} + +@enduml diff --git a/documents/images/docs/networking/commands/request.svg b/documents/images/docs/networking/commands/request.svg new file mode 100644 index 0000000..c4d8826 --- /dev/null +++ b/documents/images/docs/networking/commands/request.svg @@ -0,0 +1 @@ +Requestcontext: RequestContextgetContext(): RequestContextgetSessionId(): SessionIdgetRequestId(): int \ No newline at end of file diff --git a/documents/images/docs/networking/commands/request_parameter_accessor.puml b/documents/images/docs/networking/commands/request_parameter_accessor.puml new file mode 100644 index 0000000..4c5419b --- /dev/null +++ b/documents/images/docs/networking/commands/request_parameter_accessor.puml @@ -0,0 +1,14 @@ +@startuml +skinparam backgroundColor transparent + +class RequestParameterAccessor { + - index: Map + + + RequestParameterAccessor(parameters: List) + + require(key: String): String + + require(key: String, parser: ThrowingParser): T + + optional(key: String, defaultValue: String): String + + optional(key: String, defaultValue: T, parser: ThrowingParser): T +} + +@enduml diff --git a/documents/images/docs/networking/commands/request_parameter_accessor.svg b/documents/images/docs/networking/commands/request_parameter_accessor.svg new file mode 100644 index 0000000..d9a6bce --- /dev/null +++ b/documents/images/docs/networking/commands/request_parameter_accessor.svg @@ -0,0 +1 @@ +RequestParameterAccessorindex: Map<String, String>RequestParameterAccessor(parameters: List<RequestParameters>)require(key: String): Stringrequire(key: String, parser: ThrowingParser<T>): Toptional(key: String, defaultValue: String): Stringoptional(key: String, defaultValue: T, parser: ThrowingParser<T>): T \ No newline at end of file diff --git a/documents/images/docs/networking/commands/response_types.puml b/documents/images/docs/networking/commands/response_types.puml new file mode 100644 index 0000000..51d5733 --- /dev/null +++ b/documents/images/docs/networking/commands/response_types.puml @@ -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 diff --git a/documents/images/docs/networking/commands/response_types.svg b/documents/images/docs/networking/commands/response_types.svg new file mode 100644 index 0000000..142696c --- /dev/null +++ b/documents/images/docs/networking/commands/response_types.svg @@ -0,0 +1 @@ +Responsecontext: RequestContextbody: ResponseBodyprefix(): StringgetSessionId(): SessionIdgetRequestId(): intgetBody(): ResponseBodySuccessResponseprefix(): String (+OK)OkResponseErrorResponseprefix(): String (-ERR) \ No newline at end of file diff --git a/documents/images/docs/networking/commands/sequence_diagram.puml b/documents/images/docs/networking/commands/sequence_diagram.puml new file mode 100644 index 0000000..ee932eb --- /dev/null +++ b/documents/images/docs/networking/commands/sequence_diagram.puml @@ -0,0 +1,20 @@ +@startuml +skinparam backgroundColor transparent + +participant SessionReader +participant CommandParserDispatcher as Dispatcher +participant "CommandParser" as Parser +participant CommandRouter as Router +participant "CommandHandler" 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 diff --git a/documents/images/docs/networking/commands/sequence_diagram.svg b/documents/images/docs/networking/commands/sequence_diagram.svg new file mode 100644 index 0000000..b838d09 --- /dev/null +++ b/documents/images/docs/networking/commands/sequence_diagram.svg @@ -0,0 +1 @@ +SessionReaderCommandParserDispatcherCommandParser.T.CommandRouterCommandHandler.T.ResponseDispatcherSessionReaderSessionReaderCommandParserDispatcherCommandParserDispatcherCommandParser<T>CommandParser<T>CommandRouterCommandRouterCommandHandler<T>CommandHandler<T>ResponseDispatcherResponseDispatcherparse(primitiveRequest)parse(primitiveRequest)ExampleRequestRequestexecute(request)execute(ExampleRequest)dispatch(ExampleRequest) \ No newline at end of file