From 4317b63a87f0f7e0cff43cec2d9cd67781f8645f Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sat, 4 Apr 2026 14:36:46 +0200 Subject: [PATCH 1/3] Docs: Write README for commands section of serverside documentation --- documents/docs/networking/commands/README.md | 29 ++++++++++++++++ .../docs/networking/commands/overview.puml | 33 +++++++++++++++++++ .../docs/networking/commands/overview.svg | 1 + 3 files changed, 63 insertions(+) create mode 100644 documents/docs/networking/commands/README.md create mode 100644 documents/images/docs/networking/commands/overview.puml create mode 100644 documents/images/docs/networking/commands/overview.svg diff --git a/documents/docs/networking/commands/README.md b/documents/docs/networking/commands/README.md new file mode 100644 index 0000000..a7401b0 --- /dev/null +++ b/documents/docs/networking/commands/README.md @@ -0,0 +1,29 @@ +# 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 +*Link to guides* + +### Reference +*Link to technical deep dives* + +## Key Concepts +**Each command is self-contained.** +A command's Parser, Request, Handler, and Response all live in the same package under `app/commands//`. +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. diff --git a/documents/images/docs/networking/commands/overview.puml b/documents/images/docs/networking/commands/overview.puml new file mode 100644 index 0000000..112f52e --- /dev/null +++ b/documents/images/docs/networking/commands/overview.puml @@ -0,0 +1,33 @@ +@startuml +skinparam backgroundColor transparent + +package "network/ (infrastructure)" { + class PrimitiveRequest <> + interface CommandParser + interface CommandHandler + 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// (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 \ No newline at end of file diff --git a/documents/images/docs/networking/commands/overview.svg b/documents/images/docs/networking/commands/overview.svg new file mode 100644 index 0000000..8bc887d --- /dev/null +++ b/documents/images/docs/networking/commands/overview.svg @@ -0,0 +1 @@ +network/ (infrastructure)app/commands/<name>/ (per command)«record»PrimitiveRequestCommandParserT extends RequestCommandHandlerT extends RequestCommandParserDispatcherCommandRouterRequestResponseExampleParserCommandParserExampleRequestRequestExampleHandlerCommandHandlerExampleResponseResponseroutesparsed byroutes toparses toroutesroutes toexecuted bycreatesparses toexecuted byproduces \ No newline at end of file From f971e6ad5bb72697e7de14abf3c01402363b288c Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sat, 4 Apr 2026 15:51:29 +0200 Subject: [PATCH 2/3] 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 From b9a8448b53a32a976dc44bde47ae84bd99e49405 Mon Sep 17 00:00:00 2001 From: Lars Simon Winzer Date: Sat, 4 Apr 2026 18:08:03 +0200 Subject: [PATCH 3/3] Docs: Write guide on implementing a custom command and link to it from README --- documents/docs/networking/commands/README.md | 3 +- .../guide-on-implementing-a-command.md | 215 ++++++++++++++++++ 2 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 documents/docs/networking/commands/guide-on-implementing-a-command.md diff --git a/documents/docs/networking/commands/README.md b/documents/docs/networking/commands/README.md index aa0d5a9..2784975 100644 --- a/documents/docs/networking/commands/README.md +++ b/documents/docs/networking/commands/README.md @@ -10,7 +10,8 @@ The concrete implementations for each command live in `app/commands/` and are wi ## Contents ### Guides -*Link to 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) - diff --git a/documents/docs/networking/commands/guide-on-implementing-a-command.md b/documents/docs/networking/commands/guide-on-implementing-a-command.md new file mode 100644 index 0000000..037251f --- /dev/null +++ b/documents/docs/networking/commands/guide-on-implementing-a-command.md @@ -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// + 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 { + @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 { + 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.