diff --git a/README.md b/README.md index 0d8a0440..f1f1c3ec 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() @@ -147,6 +150,22 @@ async fn handle_ping(token: SessionToken, info: ConnectionInfo) { } ``` +## 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!("received request: {:?}", req); + let res = next.call(req).await?; + tracing::info!("sending response: {:?}", res); + Ok(res) +}); +``` + ## Current Limitations Connection handling now processes frames and routes messages, but the @@ -161,5 +180,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 6b811280..0006b588 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/docs/rust-binary-router-library-design.md b/docs/rust-binary-router-library-design.md index 3e0c99f6..fc0387ab 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 @@ -844,8 +842,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. @@ -919,7 +918,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 @@ -1103,57 +1101,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** @@ -1209,7 +1213,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, @@ -1228,7 +1232,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, @@ -1242,7 +1246,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 2b119de4..15933184 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -1,8 +1,8 @@ -//! Middleware traits and helpers. +//! Request middleware building blocks. //! -//! This module defines the asynchronous [`Service`] and [`Transform`] traits, -//! along with [`ServiceRequest`] and [`ServiceResponse`] wrappers. Middleware -//! components use the [`Next`] helper to call the next service in 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; @@ -127,3 +127,87 @@ 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) } + +/// 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: Arc, +} + +#[async_trait] +impl Service for FnService +where + S: Service + 'static, + 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.as_ref())(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 + 'static, + Fut: std::future::Future> + Send, +{ + type Output = FnService; + + async fn transform(&self, service: S) -> Self::Output { + FnService { + service, + f: Arc::new(self.f.clone()), + } + } +}