diff --git a/README.md b/README.md index 60a40729..cd0704dd 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 @@ -163,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; @@ -204,12 +213,26 @@ let logging = from_fn(|req, next| async move { ## Examples -The `examples/` directory contains runnable demos illustrating different -protocol designs: +Example programs are available in the `examples/` directory: -- `echo.rs` – minimal server that echoes incoming frames. +- `echo.rs` – minimal echo server using routing - `packet_enum.rs` – shows packet type discrimination with a bincode enum and a frame containing container types like `HashMap` and `Vec`. +- `ping_pong.rs` – showcases serialization and middleware in a ping/pong + protocol + +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 +$ printf '\x00\x00\x00\x08\x01\x00\x00\x00\x2a\x00\x00\x00' | nc 127.0.0.1 7878 | xxd ## Current Limitations 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.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 +``` diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs new file mode 100644 index 00000000..245bbb5b --- /dev/null +++ b/examples/ping_pong.rs @@ -0,0 +1,148 @@ +use std::{io, net::SocketAddr, sync::Arc}; + +use async_trait::async_trait; +use wireframe::{ + app::{Result as AppResult, 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); + +#[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. +/// +/// The middleware chain generates the actual response, so this +/// handler intentionally performs no work. +#[allow(clippy::unused_async)] +async fn ping_handler() {} + +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, _) = match Ping::from_bytes(req.frame()) { + Ok(val) => val, + Err(e) => { + eprintln!("failed to decode ping: {e:?}"); + return Ok(ServiceResponse::new(encode_error(format!( + "decode error: {e:?}" + )))); + } + }; + let mut response = self.inner.call(req).await?; + let pong_resp = if let Some(v) = ping_req.0.checked_add(1) { + Pong(v) + } else { + eprintln!("ping overflowed at {}", ping_req.0); + 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:?}"); + return Ok(ServiceResponse::new(encode_error(format!( + "encode error: {e:?}" + )))); + } + } + Ok(response) + } +} + +#[async_trait] +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 }) + } +} + +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; + + // `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 }) + } +} + +fn build_app() -> AppResult { + WireframeApp::new()? + .serializer(BincodeSerializer) + .route(PING_ID, Arc::new(|_| Box::pin(ping_handler())))? + .wrap(PongMiddleware)? + .wrap(Logging) +} + +#[tokio::main] +async fn main() -> io::Result<()> { + let factory = || build_app().expect("app build failed"); + + 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 +}