diff --git a/README.md b/README.md index ecabf491..d8bc453b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ connections and runs the Tokio event loop: WireframeServer::new(|| { WireframeApp::new() .frame_processor(MyFrameProcessor::new()) - .app_data(state.clone().into()) + .app_data(state.clone()) .route(MessageType::Login, handle_login) .wrap(MyLoggingMiddleware::default()) }) @@ -48,7 +48,10 @@ By default, the number of worker tasks equals the number of CPU cores. If the CPU count cannot be determined, the server falls back to a single worker. The builder supports methods like `frame_processor`, `route`, `app_data`, and -`wrap` for middleware configuration【F:docs/rust-binary-router-library-design.md†L616-L704】. +`wrap` for middleware configuration. `app_data` stores any `Send + Sync` value +keyed by type; registering another value of the same type overwrites the +previous one. Handlers retrieve these values using the `SharedState` +extractor【F:docs/rust-binary-router-library-design.md†L616-L704】. Handlers are asynchronous functions whose parameters implement extractor traits and may return responses implementing the `Responder` trait. This pattern diff --git a/docs/rust-binary-router-library-design.md b/docs/rust-binary-router-library-design.md index c4fd582e..da3e840b 100644 --- a/docs/rust-binary-router-library-design.md +++ b/docs/rust-binary-router-library-design.md @@ -626,7 +626,7 @@ WireframeApp::new() .frame_processor(MyFrameProcessor::new()) // Configure the framing logic -.app_data(app_state.clone().into()) // Shared application state +.app_data(app_state.clone()) // Shared application state //.service(login_handler) // If using attribute macros and auto-discovery @@ -662,8 +662,9 @@ inferring the message type it handles if attribute macros are used. \* .route(message_id, handler_function): Explicitly maps a message identifier to a handler. -\* .app_data(SharedState\) or .data(T): Provides shared application state, -similar to Actix Web's web::Data.21 +\* .app_data(T): Provides shared application state, keyed by type. Registering +another value of the same type replaces the previous one, mirroring Actix Web's +`web::Data`.21 \* .wrap(middleware_factory): Adds middleware to the processing pipeline.26 @@ -788,68 +789,71 @@ within handlers. ```rust - The `MessageRequest` would encapsulate information about the current incoming - message context (like connection details, already parsed headers if any), and - `Payload` would represent the raw or partially processed frame data. - ```` +The `MessageRequest` encapsulates connection metadata and any values registered +with `WireframeApp::app_data`. These values are stored by type, so only one +instance of each type can exist; later registrations overwrite earlier ones. +`Payload` represents the raw or partially processed frame data. + +````` + - **Built-in Extractors**: "wireframe" will provide several common extractors: - - `Message`: This would be the most common extractor. It attempts to - deserialize the incoming frame's payload into the specified type `T`. `T` - must implement the relevant deserialization trait (e.g., `Decode` from - `wire-rs` or `serde::Deserialize` if using `bincode`/`postcard`). +- `Message`: This would be the most common extractor. It attempts to + deserialize the incoming frame's payload into the specified type `T`. `T` + must implement the relevant deserialization trait (e.g., `Decode` from + `wire-rs` or `serde::Deserialize` if using `bincode`/`postcard`). - Rust + Rust - ````rustrust - async fn handle_user_update(update: Message) -> Result<()> { - // update.into_inner() gives UserUpdateData - //... - } + ````rustrust + async fn handle_user_update(update: Message) -> Result<()> { + // update.into_inner() returns a `UserUpdateData` instance + //... + } - ```rust + ```rust - ```` + ```` - - `ConnectionInfo`: Provides access to metadata about the current connection, - such as the peer's network address, a unique connection identifier assigned - by "wireframe", or transport-specific details. +- `ConnectionInfo`: Provides access to metadata about the current connection, + such as the peer's network address, a unique connection identifier assigned + by "wireframe", or transport-specific details. - Rust + Rust - ````rustrust - async fn handle_connect_event(conn_info: ConnectionInfo) { - println!("New connection from: {}", conn_info.peer_addr()); - } + ````rustrust + async fn handle_connect_event(conn_info: ConnectionInfo) { + println!("New connection from: {}", conn_info.peer_addr()); + } - ```rust + ```rust - ```` + ```` - - `SharedState`: Allows handlers to access shared application state that - was registered with `WireframeApp::app_data()`, similar to - `actix_web::web::Data`.21 +- `SharedState`: Allows handlers to access shared application state that + was registered with `WireframeApp::app_data()`, similar to + `actix_web::web::Data`.21 - Rust + Rust - ````rustrust - async fn get_user_count(state: SharedState>>) -> Result { - let count = state.lock().await.get_user_count(); - //... - } + ````rustrust + async fn get_user_count(state: SharedState>>) -> Result { + let count = state.lock().await.get_user_count(); + //... + } - ```rust + ```rust - ```` + ```` - **Custom Extractors**: Developers can implement `FromMessageRequest` for their - own types. This is a powerful extensibility point, allowing encapsulation of - custom logic for deriving specific pieces of data from an incoming message or - its context. For example, a custom extractor could parse a session token from - a specific field in all messages, validate it, and provide a `UserSession` - object to the handler. +own types. This is a powerful extensibility point, allowing encapsulation of +custom logic for deriving specific pieces of data from an incoming message or +its context. For example, a custom extractor could parse a session token from +a specific field in all messages, validate it, and provide a `UserSession` +object to the handler. This extractor system, backed by Rust's strong type system, ensures that handlers receive correctly typed and validated data, significantly reducing the @@ -869,41 +873,41 @@ Web's 5, allowing developers to inject custom logic into the message processing pipeline. - `WireframeMiddleware` **Concept**: Middleware in "wireframe" will be defined - by implementing a pair of traits, analogous to Actix Web's `Transform` and - `Service` traits.25 - - - The `Transform` trait would act as a factory for the middleware service. Its - `transform` method is annotated with `#[must_use]` (to encourage using the - returned service) and `#[inline]` for potential performance gains. - - The `Service` trait would define the actual request/response processing - logic. Middleware would operate on "wireframe's" internal request and - response types, which could be raw frames at one level or deserialized - messages at another, depending on the middleware's purpose. - - A simplified functional middleware approach, similar to - `actix_web::middleware::from_fn` 26, could also be provided for simpler use - cases: - - Rust - - ````rustrust - use wireframe::middleware::{Next, ServiceRequest, ServiceResponse}; // Hypothetical types - - async fn logging_mw_fn( - req: ServiceRequest, // Represents an incoming message/context - next: Next // Call to proceed to the next middleware or handler - ) -> Result { - println!("Received message: {:?}", req.message_type_id()); - let res = next.call(req).await?; // Call next service in chain - if let Some(response_info) = res.info() { - println!("Sending response: {:?}", response_info); - } - Ok(res) - } +by implementing a pair of traits, analogous to Actix Web's `Transform` and +`Service` traits.25 + +- The `Transform` trait would act as a factory for the middleware service. Its + `transform` method is annotated with `#[must_use]` (to encourage using the + returned service) and `#[inline]` for potential performance gains. +- The `Service` trait would define the actual request/response processing + logic. Middleware would operate on "wireframe's" internal request and + response types, which could be raw frames at one level or deserialized + messages at another, depending on the middleware's purpose. + +A simplified functional middleware approach, similar to +`actix_web::middleware::from_fn` 26, could also be provided for simpler use +cases: + +Rust + +````rustrust +use wireframe::middleware::{Next, ServiceRequest, ServiceResponse}; // Hypothetical types + +async fn logging_mw_fn( + req: ServiceRequest, // Represents an incoming message/context + next: Next // Call to proceed to the next middleware or handler +) -> Result { + println!("Received message: {:?}", req.message_type_id()); + let res = next.call(req).await?; // Call next service in chain + if let Some(response_info) = res.info() { + println!("Sending response: {:?}", response_info); + } + Ok(res) +} - ```rust +```rust - ```` +````` - **Registration**: Middleware would be registered with the `WireframeApp` builder: @@ -1282,7 +1286,7 @@ WireframeApp::new() .serializer(BincodeSerializer) -.app_data(SharedChatRoomState::new(chat_state.clone())) +.app_data(chat_state.clone()) .route(ChatMessageType::ClientJoin, handle_join) diff --git a/src/app.rs b/src/app.rs index c2373ee7..ada492b5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,7 +4,14 @@ //! for a [`WireframeServer`]. Most builder methods return [`Result`] //! so callers can chain registrations ergonomically. -use std::{boxed::Box, collections::HashMap, future::Future, pin::Pin, sync::Arc}; +use std::{ + any::{Any, TypeId}, + boxed::Box, + collections::HashMap, + future::Future, + pin::Pin, + sync::Arc, +}; use bytes::BytesMut; use tokio::io::{self, AsyncWrite, AsyncWriteExt}; @@ -65,6 +72,7 @@ pub struct WireframeApp>, frame_processor: BoxedFrameProcessor, serializer: S, + app_data: HashMap>, on_connect: Option>>, on_disconnect: Option>>, } @@ -131,6 +139,7 @@ where middleware: Vec::new(), frame_processor: Box::new(LengthPrefixedProcessor), serializer: S::default(), + app_data: HashMap::new(), on_connect: None, on_disconnect: None, } @@ -184,6 +193,22 @@ where Ok(self) } + /// Store a shared state value accessible to request extractors. + /// + /// The value can later be retrieved using [`SharedState`]. Registering + /// another value of the same type overwrites the previous one. + #[must_use] + pub fn app_data(mut self, state: T) -> Self + where + T: Send + Sync + 'static, + { + self.app_data.insert( + TypeId::of::(), + Arc::new(state) as Arc, + ); + self + } + /// Add a middleware component to the processing pipeline. /// /// # Errors @@ -225,6 +250,7 @@ where middleware: self.middleware, frame_processor: self.frame_processor, serializer: self.serializer, + app_data: self.app_data, on_connect: Some(Arc::new(move || Box::pin(f()))), on_disconnect: None, }) @@ -270,6 +296,7 @@ where middleware: self.middleware, frame_processor: self.frame_processor, serializer, + app_data: self.app_data, on_connect: self.on_connect, on_disconnect: self.on_disconnect, } diff --git a/src/extractor.rs b/src/extractor.rs index d5e2ae9f..0351fd1c 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -4,7 +4,12 @@ //! application state. Implement [`FromMessageRequest`] to extract data //! for handlers. -use std::{net::SocketAddr, sync::Arc}; +use std::{ + any::{Any, TypeId}, + collections::HashMap, + net::SocketAddr, + sync::Arc, +}; /// Request context passed to extractors. /// @@ -14,6 +19,27 @@ use std::{net::SocketAddr, sync::Arc}; pub struct MessageRequest { /// Address of the peer that sent the current message. pub peer_addr: Option, + /// Shared state values registered with the application. + /// + /// Values are keyed by their [`TypeId`]. Registering additional + /// state of the same type will replace the previous entry. + pub app_data: HashMap>, +} + +impl MessageRequest { + /// Retrieve shared state of type `T` if available. + /// + /// Returns `None` when no value of type `T` was registered. + #[must_use] + pub fn state(&self) -> Option> + where + T: Send + Sync + 'static, + { + self.app_data + .get(&TypeId::of::()) + .and_then(|data| data.clone().downcast::().ok()) + .map(SharedState) + } } /// Raw payload buffer handed to extractors. @@ -98,6 +124,42 @@ impl From for SharedState { fn from(inner: T) -> Self { Self(Arc::new(inner)) } } +/// Errors that can occur when extracting built-in types. +/// +/// This enum is marked `#[non_exhaustive]` so more variants may be added in +/// the future without breaking changes. +#[derive(Debug)] +#[non_exhaustive] +pub enum ExtractError { + /// No shared state of the requested type was found. + MissingState(&'static str), +} + +impl std::fmt::Display for ExtractError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MissingState(ty) => write!(f, "no shared state registered for {ty}"), + } + } +} + +impl std::error::Error for ExtractError {} + +impl FromMessageRequest for SharedState +where + T: Send + Sync + 'static, +{ + type Error = ExtractError; + + fn from_message_request( + req: &MessageRequest, + _payload: &mut Payload<'_>, + ) -> Result { + req.state::() + .ok_or(ExtractError::MissingState(std::any::type_name::())) + } +} + impl std::ops::Deref for SharedState { type Target = T; diff --git a/tests/app_data.rs b/tests/app_data.rs new file mode 100644 index 00000000..0ff1606b --- /dev/null +++ b/tests/app_data.rs @@ -0,0 +1,35 @@ +use std::{any::TypeId, collections::HashMap, sync::Arc}; + +use wireframe::extractor::{ + ExtractError, + FromMessageRequest, + MessageRequest, + Payload, + SharedState, +}; + +#[test] +fn shared_state_extractor_returns_data() { + let mut map = HashMap::new(); + map.insert( + TypeId::of::(), + Arc::new(5u32) as Arc, + ); + let req = MessageRequest { + peer_addr: None, + app_data: map, + }; + let mut payload = Payload::default(); + let extracted = SharedState::::from_message_request(&req, &mut payload).unwrap(); + assert_eq!(*extracted, 5); +} + +#[test] +fn missing_shared_state_returns_error() { + let req = MessageRequest::default(); + let mut payload = Payload::default(); + let err = SharedState::::from_message_request(&req, &mut payload) + .err() + .unwrap(); + assert!(matches!(err, ExtractError::MissingState(_))); +}