From 8fcd88263a1e927fe563ebfbdafb014c472b8bab Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 19 Jun 2025 20:05:10 +0100 Subject: [PATCH 1/3] Correct echo server example formatting Fixes broken code fences and syntax in the illustrative server example. --- README.md | 16 ++ docs/rust-binary-router-library-design.md | 192 +++++++++++----------- src/middleware.rs | 79 +++++++++ 3 files changed, 193 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index d8bc453b..a5c6723f 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,22 @@ let app = WireframeApp::new() }); ``` +## Middleware + +Middleware allows inspecting or modifying requests and responses. The +`from_fn` helper builds middleware from an async function or closure: + +```rust +use wireframe::middleware::from_fn; + +let logging = from_fn(|req, next| async move { + tracing::info!("request_frame = {:?}", req.frame()); + let res = next.call(req).await?; + tracing::info!("response_frame = {:?}", res.frame()); + Ok(res) +}); +``` + ## Current Limitations Connection processing is not implemented yet. After the optional diff --git a/docs/rust-binary-router-library-design.md b/docs/rust-binary-router-library-design.md index c08baf63..f062226a 100644 --- a/docs/rust-binary-router-library-design.md +++ b/docs/rust-binary-router-library-design.md @@ -136,9 +136,9 @@ network protocols, offering insights into effective abstractions. reduce boilerplate in protocol definitions, a core strategy for "wireframe". - `message-io`: This library provides abstractions for message-based network - communication over various transports like TCP, UDP, and WebSockets. - Notably, it offers `FramedTcp`, which prefixes messages with their size, - managing data as packets rather than a raw stream.17 This distinction between + communication over various transports like TCP, UDP, and WebSockets. Notably, + it offers `FramedTcp`, which prefixes messages with their size, managing data + as packets rather than a raw stream.17 This distinction between connection-oriented and packet-based transports, and the provision of framing solutions, is relevant to "wireframe's" design for handling frame-based protocols. @@ -233,8 +233,7 @@ of "wireframe": data extraction (extractors), and middleware is key to achieving the desired developer-friendliness and reducing source code complexity. 4. **Asynchronous Foundation**: Integration with an asynchronous runtime like - Tokio is non-negotiable for a modern, performant networking library in - Rust. + Tokio is non-negotiable for a modern, performant networking library in Rust. Given the inaccessibility of `leynos/mxd` 7, a direct benchmark of complexity reduction is not possible. Therefore, "wireframe" must demonstrate its benefits @@ -330,9 +329,9 @@ handling to be managed and customized independently. payload of incoming frames into strongly-typed Rust data structures (messages) and serializes outgoing Rust messages into byte payloads for outgoing frames. This is the primary role intended for `wire-rs` 6 or an alternative like - `bincode` 11 or `postcard`.12 A minimal wrapper trait in the library - currently exposes these derives under a convenient `Message` trait, - providing `to_bytes` and `from_bytes` helpers. + `bincode` 11 or `postcard`.12 A minimal wrapper trait in the library currently + exposes these derives under a convenient `Message` trait, providing `to_bytes` + and `from_bytes` helpers. - **Routing Engine**: After a message is deserialized (or at least a header containing an identifier is processed), the routing engine inspects it to determine which user-defined handler function is responsible for processing @@ -495,7 +494,6 @@ mechanism to dispatch this message to the appropriate user-defined handler. 1. **Programmatic Registration**: - ```rust // Assuming MessageType is an enum identifying different messages Router::new() @@ -538,15 +536,15 @@ mechanism to dispatch this message to the appropriate user-defined handler. could incorporate a "guard" system, analogous to Actix Web's route guards. Guards would be functions that evaluate conditions on the incoming message or connection context before a handler is chosen. - - ```rust + + ````rust Router::new() - .message_guarded( - MessageType::GenericCommand, - |msg_header: &CommandHeader| msg_header.sub_type == CommandSubType::Special, - handle_special_command - ).message(MessageType::GenericCommand, handle_generic_command) // Fallback - ``` + .message_guarded( + MessageType::GenericCommand, + + | msg_header: &CommandHeader | msg_header.sub_type == CommandSubType::Special, handle_special_command ).message(MessageType::GenericCommand, handle_generic_command) // Fallback ``` | + + ```` The routing mechanism essentially implements a form of pattern matching or a state machine that operates on message identifiers. A clear, declarative API for @@ -573,7 +571,7 @@ to run it. - `WireframeApp` **or** `Router` **Builder**: A central builder struct, let's call it `WireframeApp`, will serve as the primary point for configuring the protocol handling logic. - + ```rust use wireframe::{WireframeApp, WireframeServer, Message, error::Result}; use my_protocol::{LoginRequest, LoginResponse, ChatMessage, AppState, MyFrameProcessor, MessageType}; @@ -611,7 +609,7 @@ to run it. .run() .await } - ``` + ``` The WireframeApp builder would offer methods like: @@ -620,14 +618,14 @@ The WireframeApp builder would offer methods like: - .frame_processor(impl FrameProcessor): Sets the framing logic. - .service(handler_function): Registers a handler function, potentially -inferring the message type it handles if attribute macros are used. + inferring the message type it handles if attribute macros are used. - .route(message_id, handler_function): Explicitly maps a message identifier to -a handler. + a handler. - .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`. + another value of the same type replaces the previous one, mirroring Actix + Web's `web::Data`. - .wrap(middleware_factory): Adds middleware to the processing pipeline. @@ -696,16 +694,16 @@ analogies, illustrating how the "aesthetic sense" of Actix Web is translated: #### Table 1: Core `wireframe` API Components and Actix Web Analogies -| `wireframe` Component | Actix Web Analogy | Purpose in `wireframe` | -| --- | --- | --- | -| `WireframeApp` / `Router` | `actix_web::App` | Overall application/service configuration, route registration, middleware, state. | -| `WireframeServer` | `actix_web::HttpServer` | Binds to network, manages connections, runs the application. | -| `#[message_handler(MsgId)]` | `#[get("/path")]` / `#[post("/path")]` | Declarative routing for handlers based on message identifiers. | -| `Message` (Extractor) | `web::Json` / `web::Payload` | Extracts and deserializes the main message payload of type `T`.| -| `ConnectionInfo` (Extractor) | `HttpRequest` | Provides access to connection-specific data (e.g., peer address, connection ID). | -| `SharedState` (Extractor) | `web::Data` | Provides access to shared application state. | -| `impl WireframeResponder` | `impl Responder` | Defines how handler return values are serialized and sent back to the client. | -| `WireframeMiddleware` (Transform) | `impl Transform` | Factory for middleware services that process messages/frames. | +| `wireframe` Component | Actix Web Analogy | Purpose in `wireframe` | +| --------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------- | +| `WireframeApp` / `Router` | `actix_web::App` | Overall application/service configuration, route registration, middleware, state. | +| `WireframeServer` | `actix_web::HttpServer` | Binds to network, manages connections, runs the application. | +| `#[message_handler(MsgId)]` | `#[get("/path")]` / `#[post("/path")]` | Declarative routing for handlers based on message identifiers. | +| `Message` (Extractor) | `web::Json` / `web::Payload` | Extracts and deserializes the main message payload of type `T`. | +| `ConnectionInfo` (Extractor) | `HttpRequest` | Provides access to connection-specific data (e.g., peer address, connection ID). | +| `SharedState` (Extractor) | `web::Data` | Provides access to shared application state. | +| `impl WireframeResponder` | `impl Responder` | Defines how handler return values are serialized and sent back to the client. | +| `WireframeMiddleware` (Transform) | `impl Transform` | Factory for middleware services that process messages/frames. | This mapping is valuable because it leverages existing mental models for developers familiar with Actix Web, thereby lowering the barrier to adoption for @@ -747,9 +745,9 @@ instance of each type can exist; later registrations overwrite earlier ones. - **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`). + 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 async fn handle_user_update(update: Message) -> Result<()> { @@ -759,8 +757,8 @@ instance of each type can exist; later registrations overwrite earlier ones. ``` - `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. + such as the peer's network address, a unique connection identifier assigned by + "wireframe", or transport-specific details. ```rust async fn handle_connect_event(conn_info: ConnectionInfo) { @@ -768,8 +766,8 @@ instance of each type can exist; later registrations overwrite earlier ones. } ``` -- `SharedState`: Allows handlers to access shared application state that - was registered with `WireframeApp::app_data()`, similar to +- `SharedState`: Allows handlers to access shared application state that was + registered with `WireframeApp::app_data()`, similar to `actix_web::web::Data`. ```rust @@ -810,8 +808,9 @@ pipeline. - 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 + +- 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. @@ -885,7 +884,6 @@ async fn logging_mw_fn( Middleware is typically executed in the reverse order of registration for incoming messages and in the registration order for outgoing responses. - - **Use Cases**: - **Logging**: Recording details of incoming and outgoing messages, connection @@ -1069,57 +1067,63 @@ examples are invaluable. They make the abstract design tangible and showcase how (Note: "wireframe" would abstract the direct use of `Encoder`/`Decoder` behind its own `FrameProcessor` trait or provide helpers.) - 3. **Server Setup and Handler**: - - ```rust - // Crate: main.rs +1. **Server Setup and Handler**: + + ```rust + // Crate: main.rs + + use wireframe::{ + WireframeApp, + WireframeServer, + Message, + error::Result as WireframeResult, + serializer::BincodeSerializer, + }; + use my_protocol_messages::{EchoRequest, EchoResponse}; + use my_frame_processor::LengthPrefixedCodec; // Or wireframe's abstraction + use std::time::{SystemTime, UNIX_EPOCH}; + + // Define a message ID enum if not using type-based routing directly + enum MyMessageType { + Echo = 1, + } - use wireframe::{ - WireframeApp, - WireframeServer, - Message, - error::Result as WireframeResult, - serializer::BincodeSerializer - }; - use my_protocol_messages::{EchoRequest, EchoResponse}; - use my_frame_processor::LengthPrefixedCodec; // Or wireframe's abstraction - use std::time::{SystemTime, UNIX_EPOCH}; - - // Define a message ID enum if not using type-based routing directly - - enum MyMessageType { Echo = 1 } - - // Handler function - async fn handle_echo(req: Message) -> WireframeResult { - println!("Received echo request with payload: {}", req.payload); - Ok(EchoResponse { - original_payload: req.payload.clone(), - echoed_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(). - }) - } - - #[tokio::main] async fn main() -> std::io::Result\<()> { println!("Starting echo server on 127.0.0.1:8000"); - - WireframeServer::new(|| { - WireframeApp::new() - //.frame_processor(LengthPrefixedCodec) // Simplified - .serializer(BincodeSerializer) // Specify serializer - .route(MyMessageType::Echo, handle_echo) // Route based on ID - // OR if type-based routing is supported and EchoRequest has an ID: - //.service(handle_echo_typed) where handle_echo_typed takes Message - }) - .bind("127.0.0.1:8000")? - .run() - .await - } - ``` + // Handler function + async fn handle_echo( + req: Message, + ) -> WireframeResult { + println!("Received echo request with payload: {}", req.payload); + Ok(EchoResponse { + original_payload: req.payload.clone(), + echoed_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }) + } - This example, even in outline, demonstrates how derive macros for messages, - a separable framing component, and a clear handler signature with - extractors (`Message`) and a return type - (`WireframeResult`) simplify server implementation. + #[tokio::main] + async fn main() -> std::io::Result<()> { + println!("Starting echo server on 127.0.0.1:8000"); + + WireframeServer::new(|| { + WireframeApp::new() + //.frame_processor(LengthPrefixedCodec) // Simplified + .serializer(BincodeSerializer) // Specify serializer + .route(MyMessageType::Echo, handle_echo) // Route based on ID + // OR if type-based routing is supported and EchoRequest has an ID: + //.service(handle_echo_typed) where handle_echo_typed takes Message + }) + .bind("127.0.0.1:8000")? + .run() + .await + } + ``` - ``` +This example, even in outline, demonstrates how derive macros for messages, a +separable framing component, and a clear handler signature with extractors +(`Message`) and a return type (`WireframeResult`) +simplify server implementation. - **Example 2: Basic Chat Message Protocol** @@ -1175,7 +1179,7 @@ examples are invaluable. They make the abstract design tangible and showcase how use ChatRoomState, SharedChatRoomState... use std::sync::Arc; enum ChatMessageType { ClientJoin = 10, ClientPost = 11 } - + async fn handle_join( msg: Message, // Assume it's ClientMessage::Join conn_info: ConnectionInfo, @@ -1194,7 +1198,7 @@ examples are invaluable. They make the abstract design tangible and showcase how reason: "Invalid Join message".to_string() })) } - + async fn handle_post( msg: Message, // Assume it's Client Message::Post conn_info: ConnectionInfo, @@ -1208,7 +1212,7 @@ examples are invaluable. They make the abstract design tangible and showcase how println!("User '{}' posted: {}", user_name, content); } } - + #[tokio::main] async fn main() -> std::io::Result<()> { let chat_state = Arc::new(Mutex::new(ChatRoomState { diff --git a/src/middleware.rs b/src/middleware.rs index 927648c0..05c7fcbd 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -87,3 +87,82 @@ where #[must_use = "use the returned middleware service"] async fn transform(&self, service: S) -> Self::Output; } + +/// Middleware created from an asynchronous function. +/// +/// The function receives a [`ServiceRequest`] and a [`Next`] reference to invoke +/// the remaining middleware chain. It must return a [`ServiceResponse`] wrapped +/// in a [`Result`]. The error type is the same as the wrapped service. +pub struct FromFn { + f: F, +} + +impl FromFn { + /// Construct middleware from the provided asynchronous function. + pub fn new(f: F) -> Self { Self { f } } +} + +/// Convenience constructor to build middleware from an async function. +/// +/// # Examples +/// +/// ``` +/// use wireframe::middleware::{from_fn, ServiceRequest, ServiceResponse, Next}; +/// +/// async fn logging(req: ServiceRequest, next: Next<'_, MyService>) +/// -> Result +/// { +/// println!("request: {:?}", req); +/// let res = next.call(req).await?; +/// println!("response: {:?}", res); +/// Ok(res) +/// } +/// +/// # struct MyService; +/// # #[async_trait::async_trait] +/// # impl wireframe::middleware::Service for MyService { +/// # type Error = std::convert::Infallible; +/// # async fn call(&self, _req: ServiceRequest) -> Result { +/// # Ok(ServiceResponse) +/// # } +/// # } +/// let mw = from_fn(logging); +/// ``` +pub fn from_fn(f: F) -> FromFn { FromFn::new(f) } + +pub struct FnService { + service: S, + f: F, +} + +#[async_trait] +impl Service for FnService +where + S: Service + 'static, + F: for<'a> Fn(ServiceRequest, Next<'a, S>) -> Fut + Send + Sync + Clone, + Fut: std::future::Future> + Send, +{ + type Error = S::Error; + + async fn call(&self, req: ServiceRequest) -> Result { + let next = Next::new(&self.service); + (self.f.clone())(req, next).await + } +} + +#[async_trait] +impl Transform for FromFn +where + S: Service + 'static, + F: for<'a> Fn(ServiceRequest, Next<'a, S>) -> Fut + Send + Sync + Clone, + Fut: std::future::Future> + Send, +{ + type Output = FnService; + + async fn transform(&self, service: S) -> Self::Output { + FnService { + service, + f: self.f.clone(), + } + } +} From 16398e3218a2043a0fe42dd038b6a0d6a1d44264 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 20 Jun 2025 02:37:37 +0100 Subject: [PATCH 2/3] Add module docs for middleware --- src/middleware.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/middleware.rs b/src/middleware.rs index 05c7fcbd..364ec2b1 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -1,7 +1,8 @@ -//! Traits and helpers for request middleware. +//! Request middleware building blocks. //! -//! Middleware components implement [`Transform`] to wrap services and -//! process `ServiceRequest` instances before passing them along the chain. +//! Middleware can inspect or modify [`ServiceRequest`] values before they reach +//! the underlying [`Service`]. Implement [`Transform`] to wrap services or use +//! [`from_fn`] to create middleware from an async function. use async_trait::async_trait; From c7a0f221e3e09c46d5d68766a2f17b453669387b Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 20 Jun 2025 04:25:52 +0100 Subject: [PATCH 3/3] Update from_fn example and middleware --- README.md | 38 ++++++++++++++++++++------------------ docs/roadmap.md | 4 ++-- src/middleware.rs | 17 ++++++++++++----- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index a5c6723f..a80778b1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ reduce this boilerplate through layered abstractions: - **Transport adapter** built on Tokio I/O - **Framing layer** for length‑prefixed or custom frames -- **Connection preamble** with customizable validation callbacks \[[docs](docs/preamble-validator.md)\] +- **Connection preamble** with customizable validation callbacks + \[[docs](docs/preamble-validator.md)\] - Call `with_preamble::()` before registering success or failure callbacks - **Serialization engine** using `bincode` or a `wire-rs` wrapper - **Routing engine** that dispatches messages by ID @@ -55,7 +56,8 @@ 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 -mirrors Actix Web handlers and keeps protocol logic concise【F:docs/rust-binary-router-library-design.md†L676-L704】. +mirrors Actix Web handlers and keeps protocol logic +concise【F:docs/rust-binary-router-library-design.md†L676-L704】. ## Example @@ -86,17 +88,18 @@ binary protocol server【F:docs/rust-binary-router-library-design.md†L1120-L11 ## Response Serialization and Framing Handlers can return types implementing the `Responder` trait. These values are -encoded using the application's configured serializer and written -back through the `FrameProcessor`【F:docs/rust-binary-router-library-design.md†L718-L724】. +encoded using the application's configured serializer and written back through +the `FrameProcessor`【F:docs/rust-binary-router-library-design.md†L718-L724】. The included `LengthPrefixedProcessor` illustrates a simple framing strategy -based on a big‑endian length prefix【F:docs/rust-binary-router-library-design.md†L1076-L1117】. +based on a big‑endian length +prefix【F:docs/rust-binary-router-library-design.md†L1076-L1117】. ## Connection Lifecycle -`WireframeApp` can run callbacks when a connection is opened or closed. The state -produced by `on_connection_setup` is passed to `on_connection_teardown` when the -connection ends. +`WireframeApp` can run callbacks when a connection is opened or closed. The +state produced by `on_connection_setup` is passed to `on_connection_teardown` +when the connection ends. ```rust let app = WireframeApp::new() @@ -108,26 +111,25 @@ let app = WireframeApp::new() ## Middleware -Middleware allows inspecting or modifying requests and responses. The -`from_fn` helper builds middleware from an async function or closure: +Middleware allows inspecting or modifying requests and responses. The `from_fn` +helper builds middleware from an async function or closure: ```rust use wireframe::middleware::from_fn; let logging = from_fn(|req, next| async move { - tracing::info!("request_frame = {:?}", req.frame()); + tracing::info!("received request: {:?}", req); let res = next.call(req).await?; - tracing::info!("response_frame = {:?}", res.frame()); + tracing::info!("sending response: {:?}", res); Ok(res) }); ``` ## Current Limitations -Connection processing is not implemented yet. After the optional -preamble is read, the server logs a warning and immediately closes the -stream. Release builds fail to compile to prevent accidental production -use. +Connection processing is not implemented yet. After the optional preamble is +read, the server logs a warning and immediately closes the stream. Release +builds fail to compile to prevent accidental production use. ## Roadmap @@ -137,5 +139,5 @@ extractor traits, and providing example applications【F:docs/roadmap.md†L1-L2 ## License -Wireframe is distributed under the terms of the ISC license. -See [LICENSE](LICENSE) for details. +Wireframe is distributed under the terms of the ISC license. See +[LICENSE](LICENSE) for details. diff --git a/docs/roadmap.md b/docs/roadmap.md index f376e678..249ebc9b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -107,9 +107,9 @@ after formatting. Line numbers below refer to that file. use wireframe::middleware::from_fn; let logging = from_fn(|req, next| async move { - tracing::info!("request_frame = {:?}", req.frame()); + tracing::info!("received request: {:?}", req); let mut res = next.call(req).await?; - tracing::info!("response_frame = {:?}", res.frame()); + tracing::info!("sending response: {:?}", res); Ok(res) }); ``` diff --git a/src/middleware.rs b/src/middleware.rs index 364ec2b1..21288903 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -4,6 +4,8 @@ //! the underlying [`Service`]. Implement [`Transform`] to wrap services or use //! [`from_fn`] to create middleware from an async function. +use std::sync::Arc; + use async_trait::async_trait; /// Incoming request wrapper passed through middleware. @@ -131,23 +133,28 @@ impl FromFn { /// ``` pub fn from_fn(f: F) -> FromFn { FromFn::new(f) } +/// Service wrapper that applies a middleware function to requests. +/// +/// Created by [`FromFn::transform`], this type owns the wrapped service and an +/// `Arc` to the middleware function. The function is invoked on each request +/// with a [`ServiceRequest`] and [`Next`] continuation. pub struct FnService { service: S, - f: F, + f: Arc, } #[async_trait] impl Service for FnService where S: Service + 'static, - F: for<'a> Fn(ServiceRequest, Next<'a, S>) -> Fut + Send + Sync + Clone, + F: for<'a> Fn(ServiceRequest, Next<'a, S>) -> Fut + Send + Sync + 'static, Fut: std::future::Future> + Send, { type Error = S::Error; async fn call(&self, req: ServiceRequest) -> Result { let next = Next::new(&self.service); - (self.f.clone())(req, next).await + (self.f.as_ref())(req, next).await } } @@ -155,7 +162,7 @@ where impl Transform for FromFn where S: Service + 'static, - F: for<'a> Fn(ServiceRequest, Next<'a, S>) -> Fut + Send + Sync + Clone, + F: for<'a> Fn(ServiceRequest, Next<'a, S>) -> Fut + Send + Sync + Clone + 'static, Fut: std::future::Future> + Send, { type Output = FnService; @@ -163,7 +170,7 @@ where async fn transform(&self, service: S) -> Self::Output { FnService { service, - f: self.f.clone(), + f: Arc::new(self.f.clone()), } } }