From 20e7fdcfbef50f8475159adc8e33ce89f3dc7acf Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 22 Jun 2025 00:05:59 +0100 Subject: [PATCH 1/9] Document new examples --- README.md | 8 ++++ docs/roadmap.md | 2 +- examples/ping_pong.rs | 107 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 examples/ping_pong.rs diff --git a/README.md b/README.md index 7ba9fb46..e1e74e1e 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,14 @@ let logging = from_fn(|req, next| async move { }); ``` +## Examples + +Example programs are available in the `examples/` directory: + +- `echo.rs` – minimal echo server using routing +- `ping_pong.rs` – showcases serialization and middleware in a ping/pong + protocol + ## Current Limitations Connection handling now processes frames and routes messages, but the server is diff --git a/docs/roadmap.md b/docs/roadmap.md index 70f2c5bb..6da6bf35 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -117,7 +117,7 @@ after formatting. Line numbers below refer to that file. ## 3. Initial Examples and Documentation -- [ ] Provide examples demonstrating routing, serialization, and middleware. +- [x] Provide examples demonstrating routing, serialization, and middleware. Document configuration and usage reflecting the API design section. ## 4. Extended Features diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs new file mode 100644 index 00000000..2fe482b9 --- /dev/null +++ b/examples/ping_pong.rs @@ -0,0 +1,107 @@ +use std::{io, sync::Arc}; + +use async_trait::async_trait; +use wireframe::{ + app::{Envelope, WireframeApp}, + message::Message, + middleware::{HandlerService, Service, ServiceRequest, ServiceResponse, Transform}, + serializer::BincodeSerializer, + server::WireframeServer, +}; + +#[derive(bincode::Encode, bincode::BorrowDecode, Debug)] +struct Ping(u32); + +#[derive(bincode::Encode, bincode::BorrowDecode, Debug)] +struct Pong(u32); + +const PING_ID: u32 = 1; + +fn ping_handler( + _env: &Envelope, +) -> std::pin::Pin + Send>> { + Box::pin(async {}) +} + +struct PongMiddleware; + +struct PongService { + inner: S, +} + +#[async_trait] +impl Service for PongService +where + S: Service + Send + Sync + 'static, +{ + type Error = std::convert::Infallible; + + async fn call(&self, req: ServiceRequest) -> Result { + let (ping_req, _) = Ping::from_bytes(req.frame()).unwrap(); + let mut response = self.inner.call(req).await?; + let pong_resp = Pong(ping_req.0 + 1); + *response.frame_mut() = pong_resp.to_bytes().unwrap(); + Ok(response) + } +} + +#[async_trait] +impl Transform for PongMiddleware { + type Output = HandlerService; + + async fn transform(&self, service: HandlerService) -> Self::Output { + let id = service.id(); + HandlerService::from_service(id, PongService { inner: service }) + } +} + +struct Logging; + +struct LoggingService { + inner: S, +} + +#[async_trait] +impl Service for LoggingService +where + S: Service + Send + Sync + 'static, +{ + type Error = std::convert::Infallible; + + async fn call(&self, req: ServiceRequest) -> Result { + println!("request: {:?}", req.frame()); + let resp = self.inner.call(req).await?; + println!("response: {:?}", resp.frame()); + Ok(resp) + } +} + +#[async_trait] +impl Transform for Logging { + type Output = HandlerService; + + async fn transform(&self, service: HandlerService) -> Self::Output { + let id = service.id(); + HandlerService::from_service(id, LoggingService { inner: service }) + } +} + +#[tokio::main] +async fn main() -> io::Result<()> { + let factory = || { + WireframeApp::new() + .unwrap() + .serializer(BincodeSerializer) + .route(PING_ID, Arc::new(ping_handler)) + .unwrap() + .wrap(PongMiddleware) + .unwrap() + .wrap(Logging) + .unwrap() + }; + + WireframeServer::new(factory) + .bind("127.0.0.1:7878".parse().unwrap())? + .run() + .await +} From df8d1a67ebbe5438f7039d2ef33846f32e552667 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 22 Jun 2025 00:43:59 +0100 Subject: [PATCH 2/9] Refine ping-pong example --- examples/ping_pong.rs | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs index 2fe482b9..dc07569f 100644 --- a/examples/ping_pong.rs +++ b/examples/ping_pong.rs @@ -9,6 +9,14 @@ use wireframe::{ server::WireframeServer, }; +/// Convenience helper to convert a service into a `HandlerService`. +fn wrap_service(id: u32, svc: S) -> HandlerService +where + S: Service + Send + Sync + 'static, +{ + HandlerService::from_service(id, svc) +} + #[derive(bincode::Encode, bincode::BorrowDecode, Debug)] struct Ping(u32); @@ -17,6 +25,10 @@ struct Pong(u32); const PING_ID: u32 = 1; +/// Handler invoked for `PING_ID` messages. +/// +/// The middleware chain generates the actual response, so this +/// handler intentionally performs no work. fn ping_handler( _env: &Envelope, ) -> std::pin::Pin + Send>> { @@ -37,10 +49,22 @@ where type Error = std::convert::Infallible; async fn call(&self, req: ServiceRequest) -> Result { - let (ping_req, _) = Ping::from_bytes(req.frame()).unwrap(); + let (ping_req, _) = match Ping::from_bytes(req.frame()) { + Ok(val) => val, + Err(e) => { + eprintln!("failed to decode ping: {e:?}"); + return Ok(ServiceResponse::default()); + } + }; let mut response = self.inner.call(req).await?; let pong_resp = Pong(ping_req.0 + 1); - *response.frame_mut() = pong_resp.to_bytes().unwrap(); + match pong_resp.to_bytes() { + Ok(bytes) => *response.frame_mut() = bytes, + Err(e) => { + eprintln!("failed to encode pong: {e:?}"); + return Ok(ServiceResponse::default()); + } + } Ok(response) } } @@ -51,7 +75,7 @@ impl Transform for PongMiddleware { async fn transform(&self, service: HandlerService) -> Self::Output { let id = service.id(); - HandlerService::from_service(id, PongService { inner: service }) + wrap_service(id, PongService { inner: service }) } } @@ -82,7 +106,7 @@ impl Transform for Logging { async fn transform(&self, service: HandlerService) -> Self::Output { let id = service.id(); - HandlerService::from_service(id, LoggingService { inner: service }) + wrap_service(id, LoggingService { inner: service }) } } From 423eca46a4eafeea315fad9be4dfda19bcdd3181 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 22 Jun 2025 01:40:53 +0100 Subject: [PATCH 3/9] Refine ping-pong example --- README.md | 8 ++++++ examples/ping_pong.rs | 66 +++++++++++++++++++++---------------------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index e1e74e1e..28f3c91c 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,14 @@ Example programs are available in the `examples/` directory: - `ping_pong.rs` – showcases serialization and middleware in a ping/pong protocol +Run an example with Cargo: + +```bash +$ cargo run --example ping_pong +# in another terminal +$ printf '\x00\x00\x00\x08\x01\x00\x00\x00\x2a\x00\x00\x00' | nc 127.0.0.1 7878 | xxd +``` + ## Current Limitations Connection handling now processes frames and routes messages, but the server is diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs index dc07569f..4200dbc3 100644 --- a/examples/ping_pong.rs +++ b/examples/ping_pong.rs @@ -2,38 +2,30 @@ use std::{io, sync::Arc}; use async_trait::async_trait; use wireframe::{ - app::{Envelope, WireframeApp}, + app::WireframeApp, message::Message, middleware::{HandlerService, Service, ServiceRequest, ServiceResponse, Transform}, serializer::BincodeSerializer, server::WireframeServer, }; -/// Convenience helper to convert a service into a `HandlerService`. -fn wrap_service(id: u32, svc: S) -> HandlerService -where - S: Service + Send + Sync + 'static, -{ - HandlerService::from_service(id, svc) -} - #[derive(bincode::Encode, bincode::BorrowDecode, Debug)] struct Ping(u32); #[derive(bincode::Encode, bincode::BorrowDecode, Debug)] struct Pong(u32); +#[derive(bincode::Encode, bincode::BorrowDecode, Debug)] +struct ErrorMsg(String); + const PING_ID: u32 = 1; /// Handler invoked for `PING_ID` messages. /// /// The middleware chain generates the actual response, so this /// handler intentionally performs no work. -fn ping_handler( - _env: &Envelope, -) -> std::pin::Pin + Send>> { - Box::pin(async {}) -} +#[allow(clippy::unused_async)] +async fn ping_handler() {} struct PongMiddleware; @@ -53,7 +45,9 @@ where Ok(val) => val, Err(e) => { eprintln!("failed to decode ping: {e:?}"); - return Ok(ServiceResponse::default()); + let err = ErrorMsg(format!("decode error: {e:?}")); + let bytes = err.to_bytes().unwrap_or_default(); + return Ok(ServiceResponse::new(bytes)); } }; let mut response = self.inner.call(req).await?; @@ -62,7 +56,9 @@ where Ok(bytes) => *response.frame_mut() = bytes, Err(e) => { eprintln!("failed to encode pong: {e:?}"); - return Ok(ServiceResponse::default()); + let err = ErrorMsg(format!("encode error: {e:?}")); + let bytes = err.to_bytes().unwrap_or_default(); + return Ok(ServiceResponse::new(bytes)); } } Ok(response) @@ -75,7 +71,7 @@ impl Transform for PongMiddleware { async fn transform(&self, service: HandlerService) -> Self::Output { let id = service.id(); - wrap_service(id, PongService { inner: service }) + HandlerService::from_service(id, PongService { inner: service }) } } @@ -106,26 +102,28 @@ impl Transform for Logging { async fn transform(&self, service: HandlerService) -> Self::Output { let id = service.id(); - wrap_service(id, LoggingService { inner: service }) + HandlerService::from_service(id, LoggingService { inner: service }) } } +fn build_app() -> WireframeApp { + WireframeApp::new() + .expect("failed to create app") + .serializer(BincodeSerializer) + .route(PING_ID, Arc::new(|_| Box::pin(ping_handler()))) + .expect("failed to register ping handler") + .wrap(PongMiddleware) + .expect("failed to apply PongMiddleware") + .wrap(Logging) + .expect("failed to apply Logging middleware") +} + #[tokio::main] async fn main() -> io::Result<()> { - let factory = || { - WireframeApp::new() - .unwrap() - .serializer(BincodeSerializer) - .route(PING_ID, Arc::new(ping_handler)) - .unwrap() - .wrap(PongMiddleware) - .unwrap() - .wrap(Logging) - .unwrap() - }; - - WireframeServer::new(factory) - .bind("127.0.0.1:7878".parse().unwrap())? - .run() - .await + let factory = || build_app(); + + let addr = "127.0.0.1:7878" + .parse() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; + WireframeServer::new(factory).bind(addr)?.run().await } From 699f1526fbea29b6e9608b882e6d21157b580d8b Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 22 Jun 2025 02:02:59 +0100 Subject: [PATCH 4/9] Refactor ping-pong builder to use Result --- examples/ping_pong.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs index 4200dbc3..f4f9fd4f 100644 --- a/examples/ping_pong.rs +++ b/examples/ping_pong.rs @@ -2,7 +2,7 @@ use std::{io, sync::Arc}; use async_trait::async_trait; use wireframe::{ - app::WireframeApp, + app::{Result as AppResult, WireframeApp}, message::Message, middleware::{HandlerService, Service, ServiceRequest, ServiceResponse, Transform}, serializer::BincodeSerializer, @@ -106,21 +106,17 @@ impl Transform for Logging { } } -fn build_app() -> WireframeApp { - WireframeApp::new() - .expect("failed to create app") +fn build_app() -> AppResult { + WireframeApp::new()? .serializer(BincodeSerializer) - .route(PING_ID, Arc::new(|_| Box::pin(ping_handler()))) - .expect("failed to register ping handler") - .wrap(PongMiddleware) - .expect("failed to apply PongMiddleware") + .route(PING_ID, Arc::new(|_| Box::pin(ping_handler())))? + .wrap(PongMiddleware)? .wrap(Logging) - .expect("failed to apply Logging middleware") } #[tokio::main] async fn main() -> io::Result<()> { - let factory = || build_app(); + let factory = || build_app().expect("app build failed"); let addr = "127.0.0.1:7878" .parse() From cf28e2e0c86567031a1bc728a4659c050de90ede Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 22 Jun 2025 02:15:45 +0100 Subject: [PATCH 5/9] Handle ping overflow --- examples/ping_pong.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs index f4f9fd4f..66d783fc 100644 --- a/examples/ping_pong.rs +++ b/examples/ping_pong.rs @@ -51,13 +51,32 @@ where } }; let mut response = self.inner.call(req).await?; - let pong_resp = Pong(ping_req.0 + 1); + let pong_resp = if let Some(v) = ping_req.0.checked_add(1) { + Pong(v) + } else { + eprintln!("ping overflowed at {}", ping_req.0); + let err = ErrorMsg("overflow".into()); + let bytes = match err.to_bytes() { + Ok(b) => b, + Err(e) => { + eprintln!("failed to encode error: {e:?}"); + Vec::new() + } + }; + return Ok(ServiceResponse::new(bytes)); + }; match pong_resp.to_bytes() { Ok(bytes) => *response.frame_mut() = bytes, Err(e) => { eprintln!("failed to encode pong: {e:?}"); let err = ErrorMsg(format!("encode error: {e:?}")); - let bytes = err.to_bytes().unwrap_or_default(); + let bytes = match err.to_bytes() { + Ok(b) => b, + Err(e) => { + eprintln!("failed to encode error: {e:?}"); + Vec::new() + } + }; return Ok(ServiceResponse::new(bytes)); } } From c0d8356f3a0a1dc74228d81e6d16a334ff40e6c4 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 22 Jun 2025 08:33:31 +0100 Subject: [PATCH 6/9] Improve ping-pong example --- README.md | 6 ++++++ examples/ping_pong.rs | 47 ++++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 28f3c91c..facdd3d7 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,12 @@ Example programs are available in the `examples/` directory: Run an example with Cargo: +```bash +cargo run --example echo +``` + +Try the ping‑pong server with netcat: + ```bash $ cargo run --example ping_pong # in another terminal diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs index 66d783fc..ef967a64 100644 --- a/examples/ping_pong.rs +++ b/examples/ping_pong.rs @@ -1,4 +1,4 @@ -use std::{io, sync::Arc}; +use std::{io, net::SocketAddr, sync::Arc}; use async_trait::async_trait; use wireframe::{ @@ -18,6 +18,17 @@ struct Pong(u32); #[derive(bincode::Encode, bincode::BorrowDecode, Debug)] struct ErrorMsg(String); +fn encode_error(msg: impl Into) -> Vec { + let err = ErrorMsg(msg.into()); + match err.to_bytes() { + Ok(bytes) => bytes, + Err(e) => { + eprintln!("failed to encode error: {e:?}"); + Vec::new() + } + } +} + const PING_ID: u32 = 1; /// Handler invoked for `PING_ID` messages. @@ -45,9 +56,9 @@ where Ok(val) => val, Err(e) => { eprintln!("failed to decode ping: {e:?}"); - let err = ErrorMsg(format!("decode error: {e:?}")); - let bytes = err.to_bytes().unwrap_or_default(); - return Ok(ServiceResponse::new(bytes)); + return Ok(ServiceResponse::new(encode_error(format!( + "decode error: {e:?}" + )))); } }; let mut response = self.inner.call(req).await?; @@ -55,29 +66,15 @@ where Pong(v) } else { eprintln!("ping overflowed at {}", ping_req.0); - let err = ErrorMsg("overflow".into()); - let bytes = match err.to_bytes() { - Ok(b) => b, - Err(e) => { - eprintln!("failed to encode error: {e:?}"); - Vec::new() - } - }; - return Ok(ServiceResponse::new(bytes)); + return Ok(ServiceResponse::new(encode_error("overflow"))); }; match pong_resp.to_bytes() { Ok(bytes) => *response.frame_mut() = bytes, Err(e) => { eprintln!("failed to encode pong: {e:?}"); - let err = ErrorMsg(format!("encode error: {e:?}")); - let bytes = match err.to_bytes() { - Ok(b) => b, - Err(e) => { - eprintln!("failed to encode error: {e:?}"); - Vec::new() - } - }; - return Ok(ServiceResponse::new(bytes)); + return Ok(ServiceResponse::new(encode_error(format!( + "encode error: {e:?}" + )))); } } Ok(response) @@ -137,7 +134,11 @@ fn build_app() -> AppResult { async fn main() -> io::Result<()> { let factory = || build_app().expect("app build failed"); - let addr = "127.0.0.1:7878" + let default_addr = "127.0.0.1:7878"; + let addr_str = std::env::args() + .nth(1) + .unwrap_or_else(|| default_addr.into()); + let addr: SocketAddr = addr_str .parse() .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; WireframeServer::new(factory).bind(addr)?.run().await From b1b7219eab79fbb04dece267daf72532aa2c5c46 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 22 Jun 2025 20:44:35 +0100 Subject: [PATCH 7/9] Add docs for ping-pong example --- README.md | 7 +++--- examples/ping_pong.md | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 examples/ping_pong.md diff --git a/README.md b/README.md index 20d5f514..a66bb0e1 100644 --- a/README.md +++ b/README.md @@ -153,9 +153,10 @@ incoming \[`MessageRequest`\] and remaining \[`Payload`\]. Built‑in extractors like `Message`, `SharedState` and `ConnectionInfo` decode the payload, access app state or expose peer information. -Custom extractors let you centralize parsing and validation logic that would -otherwise be duplicated across handlers. A session token parser, for example, -can verify the token before any route-specific code executes +- `echo.rs` — minimal echo server using routing +- `ping_pong.rs` — showcases serialization and middleware in a ping/pong + protocol. See [examples/ping_pong.md](examples/ping_pong.md) for a detailed + overview. [Design Guide: Data Extraction and Type Safety][data-extraction-guide]. ```rust diff --git a/examples/ping_pong.md b/examples/ping_pong.md new file mode 100644 index 00000000..ae3d9f6f --- /dev/null +++ b/examples/ping_pong.md @@ -0,0 +1,53 @@ +# Ping-Pong Example + +This example demonstrates routing, serialization, and middleware usage in a +small ping/pong protocol. The server accepts a `Ping` message containing a +counter and responds with a `Pong` containing the incremented value. Logging +middleware prints each request and response. + +```mermaid +classDiagram + class Ping { + +u32 0 + +to_bytes() + +from_bytes() + } + class Pong { + +u32 0 + +to_bytes() + +from_bytes() + } + class ErrorMsg { + +String 0 + +to_bytes() + +from_bytes() + } + class PongMiddleware { + } + class PongService { + +inner: S + +call(req: ServiceRequest) Result + } + class Logging { + } + class LoggingService { + +inner: S + +call(req: ServiceRequest) Result + } + class HandlerService { + +id() + +from_service(id, service) + } + PongMiddleware --|> Transform + PongService --|> Service + Logging --|> Transform + LoggingService --|> Service + HandlerService <.. PongService : inner + HandlerService <.. LoggingService : inner + PongMiddleware ..> HandlerService : transform + Logging ..> HandlerService : transform + WireframeApp <.. build_app : factory + WireframeServer <.. main : uses + build_app --> WireframeApp + main --> WireframeServer +``` From 74351ebd1ef4b6b7b9817b5a9e505deb6a8830ca Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 22 Jun 2025 23:21:05 +0100 Subject: [PATCH 8/9] Clarify HandlerService usage --- examples/ping_pong.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs index ef967a64..245bbb5b 100644 --- a/examples/ping_pong.rs +++ b/examples/ping_pong.rs @@ -85,6 +85,8 @@ where impl Transform for PongMiddleware { type Output = HandlerService; + // `HandlerService` is a boxed trait object without generic parameters, + // so the transform signature uses the concrete type directly. async fn transform(&self, service: HandlerService) -> Self::Output { let id = service.id(); HandlerService::from_service(id, PongService { inner: service }) @@ -116,6 +118,7 @@ where impl Transform for Logging { type Output = HandlerService; + // `HandlerService` is a concrete type, not a generic wrapper. async fn transform(&self, service: HandlerService) -> Self::Output { let id = service.id(); HandlerService::from_service(id, LoggingService { inner: service }) From 0da488eb459bcf7c06a9fe837625ea41920b3201 Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 23 Jun 2025 00:55:01 +0100 Subject: [PATCH 9/9] Add netcat usage example for echo --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index a66bb0e1..cd0704dd 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,14 @@ use wireframe::extractor::{ConnectionInfo, FromMessageRequest, MessageRequest, P pub struct SessionToken(String); +Try the echo server with netcat: + +```bash +$ cargo run --example echo +# in another terminal +$ printf '\x00\x00\x00\x00\x01\x00\x00\x00' | nc 127.0.0.1 7878 | xxd +``` + impl FromMessageRequest for SessionToken { type Error = std::convert::Infallible;