ygopro-handler 0.1.3

A type-erased, plugin-based message handler framework for YGOPro duel rooms.
Documentation
# ygopro-handler

A type-erased, plugin-based message handler framework.

This crate provides the machinery to dispatch messages to registered handlers,
extract request data, and combine handler responses. Every incoming message flows
through a `Processor` and is handled by a set of handlers keyed by message type.

The extractor/handler pattern mirrors [`axum`](https://docs.rs/axum): handlers are
plain functions whose parameters are pulled from a `Bundle` via `FromRequest`,
and whose return value is converted into a response via `IntoResponse`. Any type
implementing `FromRequest` can be a parameter, and any type implementing
`IntoResponse` can be returned — exactly like axum's
[`FromRequest`](https://docs.rs/axum/latest/axum/extract/trait.FromRequest.html) and
[`IntoResponse`](https://docs.rs/axum/latest/axum/response/trait.IntoResponse.html).

Unlike axum, however, a message is not handled by a single handler but by a **chain**
of handlers (all those registered for the message key, plus the globals), and the
chain can be **aborted** mid-way via `StopFlag`. This makes it easy to layer
plugins: each plugin registers its own handlers for a message, and any one of them
can stop the chain or replace the message that the rest see.

# Design goals

- **Handlers as closures.** A handler is any callable that takes extractable
  parameters and returns a value convertible into an `extract::Response`. Up to
  16 parameters are supported; each is pulled from a `Bundle` via `FromRequest`.
- **Message key dispatch.** Each message carries a `MessageKey` (usually a `u8`
  flag). The `Processor` routes a message to the handlers registered for that
  key, plus any global handlers.
- **Chained, abortable processing.** A message flows through every handler registered
  for its key, plus the globals, in priority order. Any handler can abort the chain
  via `StopFlag` or replace the message that downstream handlers see. Their outputs
  are combined through `std::ops::Mul` on the response, letting one handler replace
  the message, another swallow it, and another terminate the room.
- **Type-erased handlers.** `SyncHandler`, `AsyncHandler` and `TowerHandler`
  erase the concrete handler type so a heterogeneous set of handlers can live in
  a single `Processor`.

# Soundness

This crate is **unsafe**. You must carefully handle the parameters to prevent
undefined behaviour, especially **dual mutable references** — a handler that takes
two `&mut` parameters can alias the same memory. The extraction uses raw-pointer
casts because the Rust compiler cannot properly calculate the required lifetimes,
and due to the architecture the crate cannot offer the same guarantees that axum's
`FromRequest` provides.

# Performance

The crate offers three handler wrappers, trading features against speed:

1. `TowerHandler` — the most feature-complete, and the slowest. Between the handler
   and the processor it inserts a tower `Service` layer (`HandlerService`), a boxed
   future (`HandlerServiceFuture`), and a `BoxCloneService`, so every call pays for
   several layers of boxing and a `oneshot` dispatch.
2. `AsyncHandler` — gives up the tower adaptation layer. It holds the handler in an
   `Arc<dyn Call>` and boxes only the future, so it drops the extra `Service` boxing
   while staying cloneable through a cheap `Arc` clone.
3. `SyncHandler` — gives up the async nature of `Handler`: its future is always
   `Ready`. In exchange it boxes nothing per call, drops the `'static` bound on the
   request, state, and response, and enables the dual-state trick
   (`handler::sync_handler::WithSubState`).

Compared with the C++ original, which dispatches messages with a direct switch or a
virtual call, every handler here pays for at least one heap-allocated future and a
type-erasure indirection (a trait object or a function pointer) per invocation.
`Processor::process` additionally awaits the whole handler chain per item, moving
the `Bundle` through it. For the hot path (game messages) this is one boxed future
per handler, which is acceptable at the rate messages are emitted.

# Example

A processor dispatches a message to the handlers registered for its key:

```
use ygopro_handler::Processor;
use ygopro_handler::TowerHandler;
use ygopro_handler::Bundle;
use ygopro_handler::State;
use ygopro_handler::extract::Request;
use ygopro_handler::extract::Response;

type Req = Request<u8, ()>;
type Res = Response<u8>;

let mut processor = Processor::<u8, Req, State, Res>::new();
processor.register(7, TowerHandler::new(0, "test", "example", |message: &u8| -> Res {
    if *message == 7 {
        Res::Replace(*message)
    } else {
        Res::Continue
    }
}));

let bundle = Bundle::new(Request { message: 7, extra: () }, State::new(), Res::Continue);
let result = tokio::runtime::Runtime::new().unwrap().block_on(processor.process_bundle(bundle, 7));
assert!(matches!(result.response, Res::Replace(7)));
```