Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
Expand All @@ -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<T>`
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
Expand Down
162 changes: 83 additions & 79 deletions docs/rust-binary-router-library-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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\<T>) 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

Expand Down Expand Up @@ -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<T>`: 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<T>`: 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`).

Comment thread
leynos marked this conversation as resolved.
Comment thread
leynos marked this conversation as resolved.
Rust
Rust

````rustrust
async fn handle_user_update(update: Message<UserUpdateData>) -> Result<()> {
// update.into_inner() gives UserUpdateData
//...
}
````rustrust
async fn handle_user_update(update: Message<UserUpdateData>) -> Result<()> {
// update.into_inner() returns a `UserUpdateData` instance
Comment thread
leynos marked this conversation as resolved.
//...
}

```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<T>`: Allows handlers to access shared application state that
was registered with `WireframeApp::app_data()`, similar to
`actix_web::web::Data<T>`.21
- `SharedState<T>`: Allows handlers to access shared application state that
was registered with `WireframeApp::app_data()`, similar to
`actix_web::web::Data<T>`.21

Rust
Rust

````rustrust
async fn get_user_count(state: SharedState<Arc<Mutex<UserStats>>>) -> Result<UserCountResponse> {
let count = state.lock().await.get_user_count();
//...
}
````rustrust
async fn get_user_count(state: SharedState<Arc<Mutex<UserStats>>>) -> Result<UserCountResponse> {
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
Expand All @@ -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<ServiceResponse, wireframe::Error> {
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<ServiceResponse, wireframe::Error> {
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:
Expand Down Expand Up @@ -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)

Expand Down
29 changes: 28 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
//! for a [`WireframeServer`]. Most builder methods return [`Result<Self>`]
//! 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};
Expand Down Expand Up @@ -65,6 +72,7 @@ pub struct WireframeApp<S: Serializer = BincodeSerializer, C: Send + 'static = (
middleware: Vec<Box<dyn Middleware>>,
frame_processor: BoxedFrameProcessor,
serializer: S,
app_data: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
on_connect: Option<Arc<ConnectionSetup<C>>>,
on_disconnect: Option<Arc<ConnectionTeardown<C>>>,
}
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -184,6 +193,22 @@ where
Ok(self)
}

/// Store a shared state value accessible to request extractors.
///
/// The value can later be retrieved using [`SharedState<T>`]. Registering
/// another value of the same type overwrites the previous one.
#[must_use]
pub fn app_data<T>(mut self, state: T) -> Self
where
T: Send + Sync + 'static,
{
self.app_data.insert(
TypeId::of::<T>(),
Arc::new(state) as Arc<dyn Any + Send + Sync>,
);
self
}

/// Add a middleware component to the processing pipeline.
///
/// # Errors
Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -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,
}
Expand Down
64 changes: 63 additions & 1 deletion src/extractor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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<SocketAddr>,
/// 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<TypeId, Arc<dyn Any + Send + Sync>>,
Comment thread
leynos marked this conversation as resolved.
}

impl MessageRequest {
/// Retrieve shared state of type `T` if available.
///
/// Returns `None` when no value of type `T` was registered.
#[must_use]
Comment thread
leynos marked this conversation as resolved.
pub fn state<T>(&self) -> Option<SharedState<T>>
where
T: Send + Sync + 'static,
{
self.app_data
Comment thread
leynos marked this conversation as resolved.
.get(&TypeId::of::<T>())
.and_then(|data| data.clone().downcast::<T>().ok())
.map(SharedState)
}
}

/// Raw payload buffer handed to extractors.
Expand Down Expand Up @@ -98,6 +124,42 @@ impl<T: Send + Sync> From<T> for SharedState<T> {
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)]
Comment thread
leynos marked this conversation as resolved.
#[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<T> FromMessageRequest for SharedState<T>
where
T: Send + Sync + 'static,
{
type Error = ExtractError;

fn from_message_request(
req: &MessageRequest,
_payload: &mut Payload<'_>,
) -> Result<Self, Self::Error> {
req.state::<T>()
.ok_or(ExtractError::MissingState(std::any::type_name::<T>()))
}
}

impl<T: Send + Sync> std::ops::Deref for SharedState<T> {
type Target = T;

Expand Down
Loading