Skip to main content

ygopro_handler/
lib.rs

1//! A type-erased, plugin-based message handler framework.
2//!
3//! This crate provides the machinery to dispatch messages to registered handlers,
4//! extract request data, and combine handler responses. Every incoming message flows
5//! through a [`Processor`] and is handled by a set of handlers keyed by message type.
6//!
7//! The extractor/handler pattern mirrors [`axum`](https://docs.rs/axum): handlers are
8//! plain functions whose parameters are pulled from a [`Bundle`] via [`FromRequest`],
9//! and whose return value is converted into a response via [`IntoResponse`]. Any type
10//! implementing [`FromRequest`] can be a parameter, and any type implementing
11//! [`IntoResponse`] can be returned — exactly like axum's
12//! [`FromRequest`](https://docs.rs/axum/latest/axum/extract/trait.FromRequest.html) and
13//! [`IntoResponse`](https://docs.rs/axum/latest/axum/response/trait.IntoResponse.html).
14//!
15//! Unlike axum, however, a message is not handled by a single handler but by a **chain**
16//! of handlers (all those registered for the message key, plus the globals), and the
17//! chain can be **aborted** mid-way via [`StopFlag`]. This makes it easy to layer
18//! plugins: each plugin registers its own handlers for a message, and any one of them
19//! can stop the chain or replace the message that the rest see.
20//!
21//! # Design goals
22//!
23//! - **Handlers as closures.** A handler is any callable that takes extractable
24//!   parameters and returns a value convertible into a [`extract::Response`]. Up to
25//!   16 parameters are supported; each is pulled from a [`Bundle`] via [`FromRequest`].
26//! - **Message key dispatch.** Each message carries a [`MessageKey`] (usually a `u8`
27//!   flag). The [`Processor`] routes a message to the handlers registered for that
28//!   key, plus any global handlers.
29//! - **Chained, abortable processing.** A message flows through every handler registered
30//!   for its key, plus the globals, in priority order. Any handler can abort the chain
31//!   via [`StopFlag`] or replace the message that downstream handlers see. Their outputs
32//!   are combined through [`std::ops::Mul`] on the response, letting one handler replace
33//!   the message, another swallow it, and another terminate the room.
34//! - **Type-erased handlers.** [`SyncHandler`], [`AsyncHandler`] and [`TowerHandler`]
35//!   erase the concrete handler type so a heterogeneous set of handlers can live in
36//!   a single [`Processor`].
37//!
38//! # Soundness
39//!
40//! This crate is **unsafe**. You must carefully handle the parameters to prevent
41//! undefined behaviour, especially **dual mutable references** — a handler that takes
42//! two `&mut` parameters can alias the same memory. The extraction uses raw-pointer
43//! casts because the Rust compiler cannot properly calculate the required lifetimes, 
44//! and due to the architecture the crate cannot offer the same guarantees that axum's
45//! `FromRequest` provides.
46//!
47//! # Performance
48//!
49//! The crate offers three handler wrappers, trading features against speed:
50//!
51//! 1. [`TowerHandler`] — the most feature-complete, and the slowest. Between the handler
52//!    and the processor it inserts a tower `Service` layer (`HandlerService`), a boxed
53//!    future (`HandlerServiceFuture`), and a `BoxCloneService`, so every call pays for
54//!    several layers of boxing and a `oneshot` dispatch.
55//! 2. [`AsyncHandler`] — gives up the tower adaptation layer. It holds the handler in an
56//!    `Arc<dyn Call>` and boxes only the future, so it drops the extra `Service` boxing
57//!    while staying cloneable through a cheap `Arc` clone.
58//! 3. [`SyncHandler`] — gives up the async nature of [`Handler`]: its future is always
59//!    `Ready`. In exchange it boxes nothing per call, drops the `'static` bound on the
60//!    request, state, and response, and enables the dual-state trick
61//!    ([`handler::sync_handler::WithSubState`]).
62//!
63//! Compared with the C++ original, which dispatches messages with a direct switch or a
64//! virtual call, every handler here pays for at least one heap-allocated future and a
65//! type-erasure indirection (a trait object or a function pointer) per invocation.
66//! [`Processor::process`] additionally awaits the whole handler chain per item, moving
67//! the [`Bundle`] through it. For the hot path (game messages) this is one boxed future
68//! per handler, which is acceptable at the rate messages are emitted.
69//!
70//! # Example
71//!
72//! A processor dispatches a message to the handlers registered for its key:
73//!
74//! ```
75//! use ygopro_handler::Processor;
76//! use ygopro_handler::TowerHandler;
77//! use ygopro_handler::Bundle;
78//! use ygopro_handler::State;
79//! use ygopro_handler::extract::Request;
80//! use ygopro_handler::extract::Response;
81//!
82//! type Req = Request<u8, ()>;
83//! type Res = Response<u8>;
84//!
85//! let mut processor = Processor::<u8, Req, State, Res>::new();
86//! processor.register(7, TowerHandler::new(0, "test", "example", |message: &u8| -> Res {
87//!     if *message == 7 {
88//!         Res::Replace(*message)
89//!     } else {
90//!         Res::Continue
91//!     }
92//! }));
93//!
94//! let bundle = Bundle::new(Request { message: 7, extra: () }, State::new(), Res::Continue);
95//! let result = tokio::runtime::Runtime::new().unwrap().block_on(processor.process_bundle(bundle, 7));
96//! assert!(matches!(result.response, Res::Replace(7)));
97//! ```
98
99#![warn(missing_docs)]
100
101mod room;
102pub mod extract;
103pub mod handler;
104pub mod processor;
105
106pub use room::*;
107pub use handler::*;
108pub use handler::tower_handler::TowerHandler;
109pub use handler::async_handler::AsyncHandler;
110pub use handler::sync_handler::SyncHandler;
111pub use processor::*;