From 69054da32a78752852cf2adfbee3d7c08f8dee14 Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 25 Aug 2025 18:32:36 +0100 Subject: [PATCH 01/15] Modularize application builder --- src/app.rs | 826 ------------------------------------------ src/app/builder.rs | 385 ++++++++++++++++++++ src/app/connection.rs | 258 +++++++++++++ src/app/envelope.rs | 166 +++++++++ src/app/error.rs | 44 +++ src/app/mod.rs | 10 + 6 files changed, 863 insertions(+), 826 deletions(-) delete mode 100644 src/app.rs create mode 100644 src/app/builder.rs create mode 100644 src/app/connection.rs create mode 100644 src/app/envelope.rs create mode 100644 src/app/error.rs create mode 100644 src/app/mod.rs diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index 28d54773..00000000 --- a/src/app.rs +++ /dev/null @@ -1,826 +0,0 @@ -//! Application builder configuring routes and middleware. -//! -//! This module defines [`WireframeApp`], an Actix-inspired builder for -//! managing connection state, routing, and middleware in a `WireframeServer`. -//! It exposes convenience methods to register handlers and lifecycle hooks. - -use std::{ - any::{Any, TypeId}, - boxed::Box, - collections::HashMap, - future::Future, - pin::Pin, - sync::Arc, -}; - -use bytes::BytesMut; -use tokio::{ - io::{self, AsyncWrite, AsyncWriteExt}, - sync::mpsc, -}; - -use crate::{ - frame::{FrameProcessor, LengthFormat, LengthPrefixedProcessor}, - hooks::{ProtocolHooks, WireframeProtocol}, - message::Message, - middleware::{HandlerService, Service, ServiceRequest, Transform}, - serializer::{BincodeSerializer, Serializer}, -}; - -type BoxedFrameProcessor = - Box, Error = io::Error> + Send + Sync>; - -/// Callback invoked when a connection is established. -/// -/// # Examples -/// -/// ```no_run -/// use std::sync::Arc; -/// -/// use wireframe::app::ConnectionSetup; -/// -/// let setup: Arc> = Arc::new(|| { -/// Box::pin(async { -/// // Perform authentication and return connection state -/// String::from("hello") -/// }) -/// }); -/// ``` -pub type ConnectionSetup = dyn Fn() -> Pin + Send>> + Send + Sync; - -/// Callback invoked when a connection is closed. -/// -/// # Examples -/// -/// ```no_run -/// use std::sync::Arc; -/// -/// use wireframe::app::ConnectionTeardown; -/// -/// let teardown: Arc> = Arc::new(|state| { -/// Box::pin(async move { -/// println!("Dropping {state}"); -/// }) -/// }); -/// ``` -pub type ConnectionTeardown = - dyn Fn(C) -> Pin + Send>> + Send + Sync; - -/// Configures routing and middleware for a `WireframeServer`. -/// -/// The builder stores registered routes, services, and middleware -/// without enforcing an ordering. Methods return [`Result`] so -/// registrations can be chained ergonomically. -pub struct WireframeApp< - S: Serializer + Send + Sync = BincodeSerializer, - C: Send + 'static = (), - E: Packet = Envelope, -> { - routes: HashMap>, - services: Vec>, - middleware: Vec, Output = HandlerService>>>, - frame_processor: BoxedFrameProcessor, - serializer: S, - app_data: HashMap>, - on_connect: Option>>, - on_disconnect: Option>>, - protocol: Option, ProtocolError = ()>>>, - push_dlq: Option>>, -} - -/// Alias for asynchronous route handlers. -/// -/// A `Handler` is an `Arc` to a function returning a [`Future`], enabling -/// asynchronous execution of message handlers. -pub type Handler = Arc Pin + Send>> + Send + Sync>; - -/// Trait representing middleware components. -pub trait Middleware: - Transform, Output = HandlerService> + Send + Sync -{ -} - -impl Middleware for T where - T: Transform, Output = HandlerService> + Send + Sync -{ -} - -/// Top-level error type for application setup. -#[derive(Debug)] -pub enum WireframeError { - /// A route with the provided identifier was already registered. - DuplicateRoute(u32), -} - -/// Errors produced when sending a handler response over a stream. -#[derive(Debug)] -pub enum SendError { - /// Serialization failed. - Serialize(Box), - /// Writing to the stream failed. - Io(io::Error), -} - -impl std::fmt::Display for SendError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SendError::Serialize(e) => write!(f, "serialization error: {e}"), - SendError::Io(e) => write!(f, "I/O error: {e}"), - } - } -} - -impl std::error::Error for SendError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - SendError::Serialize(e) => Some(&**e), - SendError::Io(e) => Some(e), - } - } -} - -impl From for SendError { - fn from(e: io::Error) -> Self { SendError::Io(e) } -} - -/// Envelope-like type used to wrap incoming and outgoing messages. -/// -/// Custom envelope types must implement this trait so [`WireframeApp`] can -/// route messages and construct responses. -/// -/// # Example -/// -/// ``` -/// use wireframe::{ -/// app::{Packet, PacketParts}, -/// message::Message, -/// }; -/// -/// #[derive(bincode::Decode, bincode::Encode)] -/// struct CustomEnvelope { -/// id: u32, -/// payload: Vec, -/// timestamp: u64, -/// } -/// -/// impl Packet for CustomEnvelope { -/// fn id(&self) -> u32 { self.id } -/// -/// fn correlation_id(&self) -> Option { None } -/// -/// fn into_parts(self) -> PacketParts { PacketParts::new(self.id, None, self.payload) } -/// -/// fn from_parts(parts: PacketParts) -> Self { -/// Self { -/// id: parts.id(), -/// payload: parts.payload(), -/// timestamp: 0, -/// } -/// } -/// } -/// ``` -pub trait Packet: Message + Send + Sync + 'static { - /// Return the message identifier used for routing. - fn id(&self) -> u32; - - /// Return the correlation identifier tying this frame to a request. - fn correlation_id(&self) -> Option; - - /// Consume the packet and return its identifier, correlation id and payload bytes. - fn into_parts(self) -> PacketParts; - - /// Construct a new packet from raw parts. - fn from_parts(parts: PacketParts) -> Self; -} - -/// Component values extracted from or used to build a [`Packet`]. -#[derive(Debug)] -pub struct PacketParts { - id: u32, - correlation_id: Option, - payload: Vec, -} - -/// Basic envelope type used by [`WireframeApp::handle_connection`]. -/// -/// Incoming frames are deserialized into an `Envelope` containing the -/// message identifier and raw payload bytes. -#[derive(bincode::Decode, bincode::Encode, Debug)] -pub struct Envelope { - pub(crate) id: u32, - pub(crate) correlation_id: Option, - pub(crate) payload: Vec, -} - -impl Envelope { - /// Create a new [`Envelope`] with the provided identifiers and payload. - #[must_use] - pub fn new(id: u32, correlation_id: Option, payload: Vec) -> Self { - Self { - id, - correlation_id, - payload, - } - } -} - -impl Packet for Envelope { - #[inline] - fn id(&self) -> u32 { self.id } - - #[inline] - fn correlation_id(&self) -> Option { self.correlation_id } - - fn into_parts(self) -> PacketParts { self.into() } - - fn from_parts(parts: PacketParts) -> Self { parts.into() } -} - -impl PacketParts { - /// Construct a new set of packet parts. - #[must_use] - pub fn new(id: u32, correlation_id: Option, payload: Vec) -> Self { - Self { - id, - correlation_id, - payload, - } - } - - #[must_use] - pub const fn id(&self) -> u32 { self.id } - - #[must_use] - pub const fn correlation_id(&self) -> Option { self.correlation_id } - - #[must_use] - pub fn payload(self) -> Vec { self.payload } - - /// Ensure a correlation identifier is present, inheriting from `source` if missing. - /// - /// # Examples - /// ``` - /// use wireframe::app::PacketParts; - /// // Inherit when missing - /// let parts = PacketParts::new(1, None, vec![]).inherit_correlation(Some(42)); - /// assert_eq!(parts.correlation_id(), Some(42)); - /// - /// // Overwrite mismatched value - /// let parts = PacketParts::new(1, Some(7), vec![]).inherit_correlation(Some(8)); - /// assert_eq!(parts.correlation_id(), Some(8)); - /// ``` - #[must_use] - pub fn inherit_correlation(mut self, source: Option) -> Self { - match (self.correlation_id, source) { - (None, cid) => self.correlation_id = cid, - (Some(cid), Some(src)) if cid != src => { - tracing::warn!( - id = self.id, - expected = src, - found = cid, - "mismatched correlation id in response", - ); - // Overwrite with the source correlation ID to ensure downstream - // consistency. - self.correlation_id = Some(src); - } - _ => {} - } - self - } -} - -impl From for PacketParts { - fn from(e: Envelope) -> Self { PacketParts::new(e.id, e.correlation_id, e.payload) } -} - -impl From for Envelope { - fn from(p: PacketParts) -> Self { - let id = p.id(); - let correlation_id = p.correlation_id(); - let payload = p.payload(); - Envelope::new(id, correlation_id, payload) - } -} - -/// Number of idle polls before terminating a connection. -const MAX_IDLE_POLLS: u32 = 50; // ~5s with 100ms timeout -/// Maximum consecutive deserialization failures before closing a connection. -const MAX_DESER_FAILURES: u32 = 10; - -/// Result type used throughout the builder API. -pub type Result = std::result::Result; - -impl Default for WireframeApp -where - S: Serializer + Default + Send + Sync, - C: Send + 'static, - E: Packet, -{ - /// - /// Initializes empty routes, services, middleware, and application data. - /// Sets the default frame processor and serializer, with no connection - /// lifecycle hooks. - fn default() -> Self { - Self { - routes: HashMap::new(), - services: Vec::new(), - middleware: Vec::new(), - frame_processor: Box::new(LengthPrefixedProcessor::new(LengthFormat::default())), - serializer: S::default(), - app_data: HashMap::new(), - on_connect: None, - on_disconnect: None, - protocol: None, - push_dlq: None, - } - } -} - -impl WireframeApp -where - E: Packet, -{ - /// Construct a new empty application builder. - /// - /// # Errors - /// - /// This function currently never returns an error but uses [`Result`] for - /// forward compatibility. - /// - /// # Examples - /// - /// ``` - /// use wireframe::app::{Packet, WireframeApp}; - /// - /// #[derive(bincode::Encode, bincode::BorrowDecode)] - /// struct MyEnv { - /// id: u32, - /// correlation_id: Option, - /// data: Vec, - /// } - /// - /// impl Packet for MyEnv { - /// fn id(&self) -> u32 { self.id } - /// fn correlation_id(&self) -> Option { self.correlation_id } - /// fn into_parts(self) -> PacketParts { - /// PacketParts::new(self.id, self.correlation_id, self.data) - /// } - /// fn from_parts(parts: PacketParts) -> Self { - /// Self { - /// id: parts.id(), - /// correlation_id: parts.correlation_id(), - /// data: parts.payload(), - /// } - /// } - /// } - /// - /// let app = WireframeApp::<_, _, MyEnv>::new().expect("failed to create app"); - /// ``` - pub fn new() -> Result { Ok(Self::default()) } - - /// Construct a new application builder using a custom envelope type. - /// - /// Deprecated: call [`WireframeApp::new`] with explicit envelope type - /// parameters. - /// - /// # Errors - /// - /// This function currently never returns an error but uses [`Result`] for - /// forward compatibility. - #[deprecated(note = "use `WireframeApp::<_, _, E>::new()` instead")] - pub fn new_with_envelope() -> Result { Self::new() } -} - -impl WireframeApp -where - S: Serializer + Send + Sync, - C: Send + 'static, - E: Packet, -{ - /// Register a route that maps `id` to `handler`. - /// - /// # Errors - /// - /// Returns [`WireframeError::DuplicateRoute`] if a handler for `id` - /// has already been registered. - pub fn route(mut self, id: u32, handler: Handler) -> Result { - if self.routes.contains_key(&id) { - return Err(WireframeError::DuplicateRoute(id)); - } - self.routes.insert(id, handler); - Ok(self) - } - - /// Register a handler discovered by attribute macros or other means. - /// - /// # Errors - /// - /// This function always succeeds currently but uses [`Result`] for - /// consistency with other builder methods. - pub fn service(mut self, handler: Handler) -> Result { - self.services.push(handler); - Ok(self) - } - - /// Store a shared state value accessible to request extractors. - /// - /// The value can later be retrieved using [`crate::extractor::SharedState`]. Registering - /// another value of the same type overwrites the previous one. - #[must_use] - pub fn app_data(mut self, state: T) -> Self - where - T: Send + Sync + 'static, - { - self.app_data.insert( - TypeId::of::(), - Arc::new(state) as Arc, - ); - self - } - - /// Add a middleware component to the processing pipeline. - /// - /// # Errors - /// - /// This function currently always succeeds. - pub fn wrap(mut self, mw: M) -> Result - where - M: Transform, Output = HandlerService> + Send + Sync + 'static, - { - self.middleware.push(Box::new(mw)); - Ok(self) - } - - /// Register a callback invoked when a new connection is established. - /// - /// The callback can perform authentication or other setup tasks and - /// returns connection-specific state stored for the connection's - /// lifetime. - /// - /// # Type Parameters - /// - /// This method changes the connection state type parameter from `C` to `C2`. - /// This means that any subsequent builder methods will operate on the new connection state type - /// `C2`. Be aware of this type transition when chaining builder methods. - /// - /// # Errors - /// - /// This function always succeeds currently but uses [`Result`] for - /// consistency with other builder methods. - pub fn on_connection_setup(self, f: F) -> Result> - where - F: Fn() -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - C2: Send + 'static, - { - Ok(WireframeApp { - routes: self.routes, - services: self.services, - 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, - protocol: self.protocol, - push_dlq: self.push_dlq, - }) - } - - /// Register a callback invoked when a connection is closed. - /// - /// The callback receives the connection state produced by - /// [`on_connection_setup`](Self::on_connection_setup). - /// - /// # Errors - /// - /// This function always succeeds currently but uses [`Result`] for - /// consistency with other builder methods. - pub fn on_connection_teardown(mut self, f: F) -> Result - where - F: Fn(C) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - { - self.on_disconnect = Some(Arc::new(move |c| Box::pin(f(c)))); - Ok(self) - } - - /// Install a [`WireframeProtocol`] implementation. - /// - /// The protocol defines hooks for connection setup, frame modification, and - /// command completion. It is wrapped in an [`Arc`] and stored for later use - /// by the connection actor. - #[must_use] - pub fn with_protocol

(mut self, protocol: P) -> Self - where - P: WireframeProtocol, ProtocolError = ()> + 'static, - { - self.protocol = Some(Arc::new(protocol)); - self - } - - /// Configure a Dead Letter Queue for dropped push frames. - /// - /// ```rust,no_run - /// use tokio::sync::mpsc; - /// use wireframe::app::WireframeApp; - /// - /// # fn build() -> WireframeApp { WireframeApp::new().unwrap() } - /// # fn main() { - /// let (tx, _rx) = mpsc::channel(16); - /// let app = build().with_push_dlq(tx); - /// # let _ = app; - /// # } - /// ``` - #[must_use] - pub fn with_push_dlq(mut self, dlq: mpsc::Sender>) -> Self { - self.push_dlq = Some(dlq); - self - } - - /// Get a clone of the configured protocol, if any. - /// - /// Returns `None` if no protocol was installed via [`with_protocol`](Self::with_protocol). - #[must_use] - pub fn protocol( - &self, - ) -> Option, ProtocolError = ()>>> { - self.protocol.as_ref().map(Arc::clone) - } - - /// Return protocol hooks derived from the installed protocol. - /// - /// If no protocol is installed, returns default (no-op) hooks. - #[must_use] - pub fn protocol_hooks(&self) -> ProtocolHooks, ()> { - self.protocol - .as_ref() - .map(|p| ProtocolHooks::from_protocol(&Arc::clone(p))) - .unwrap_or_default() - } - - /// Set the frame processor used for encoding and decoding frames. - #[must_use] - pub fn frame_processor

(mut self, processor: P) -> Self - where - P: FrameProcessor, Error = io::Error> + Send + Sync + 'static, - { - self.frame_processor = Box::new(processor); - self - } - - /// Replace the serializer used for messages. - #[must_use] - pub fn serializer(self, serializer: Ser) -> WireframeApp - where - Ser: Serializer + Send + Sync, - { - WireframeApp { - routes: self.routes, - services: self.services, - middleware: self.middleware, - frame_processor: self.frame_processor, - serializer, - app_data: self.app_data, - on_connect: self.on_connect, - on_disconnect: self.on_disconnect, - protocol: self.protocol, - push_dlq: self.push_dlq, - } - } - - /// Serialize `msg` and write it to `stream` using the frame processor. - /// - /// # Errors - /// - /// Returns a [`SendError`] if serialization or writing fails. - pub async fn send_response( - &self, - stream: &mut W, - msg: &M, - ) -> std::result::Result<(), SendError> - where - W: AsyncWrite + Unpin, - M: Message, - { - let bytes = self - .serializer - .serialize(msg) - .map_err(SendError::Serialize)?; - let mut framed = BytesMut::with_capacity(4 + bytes.len()); - self.frame_processor - .encode(&bytes, &mut framed) - .map_err(SendError::Io)?; - stream.write_all(&framed).await.map_err(SendError::Io)?; - stream.flush().await.map_err(SendError::Io) - } -} - -impl WireframeApp -where - S: Serializer + crate::frame::FrameMetadata + Send + Sync, - C: Send + 'static, - E: Packet, -{ - /// Try parsing the frame using [`FrameMetadata::parse`], falling back to - /// full deserialization on failure. - fn parse_envelope( - &self, - frame: &[u8], - ) -> std::result::Result<(Envelope, usize), Box> { - self.serializer - .parse(frame) - .map_err(|e| Box::new(e) as Box) - .or_else(|_| self.serializer.deserialize::(frame)) - } - - /// Handle an accepted connection. - /// - /// This placeholder immediately closes the connection after the - /// preamble phase. A warning is logged so tests and callers are - /// aware of the current limitation. - pub async fn handle_connection(&self, mut stream: W) - where - W: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static, - { - let state = if let Some(setup) = &self.on_connect { - Some((setup)().await) - } else { - None - }; - - let routes = self.build_chains().await; - - if let Err(e) = self.process_stream(&mut stream, &routes).await { - tracing::warn!(correlation_id = ?None::, error = ?e, "connection terminated with error"); - } - - if let (Some(teardown), Some(state)) = (&self.on_disconnect, state) { - teardown(state).await; - } - } - - async fn build_chains(&self) -> HashMap> { - let mut routes = HashMap::new(); - for (&id, handler) in &self.routes { - let mut service = HandlerService::new(id, handler.clone()); - for mw in self.middleware.iter().rev() { - service = mw.transform(service).await; - } - routes.insert(id, service); - } - routes - } - - async fn process_stream( - &self, - stream: &mut W, - routes: &HashMap>, - ) -> io::Result<()> - where - W: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin, - { - let mut buf = BytesMut::with_capacity(1024); - let mut idle = 0u32; - let mut deser_failures = 0u32; - - loop { - if let Some(frame) = self.frame_processor.decode(&mut buf)? { - self.handle_frame(stream, &frame, &mut deser_failures, routes) - .await?; - idle = 0; - continue; - } - - if self.read_and_update(stream, &mut buf, &mut idle).await? { - break; - } - } - - Ok(()) - } - - async fn read_and_update( - &self, - stream: &mut W, - buf: &mut BytesMut, - idle: &mut u32, - ) -> io::Result - where - W: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin, - { - match self.read_into(stream, buf).await { - Ok(Some(0)) => Ok(true), - Ok(Some(_)) => { - *idle = 0; - Ok(false) - } - Ok(None) => { - *idle += 1; - Ok(*idle >= MAX_IDLE_POLLS) - } - Err(e) if Self::is_transient_error(&e) => Ok(false), - Err(e) if Self::is_fatal_error(&e) => Ok(true), - Err(e) => Err(e), - } - } - - fn is_transient_error(e: &io::Error) -> bool { - matches!( - e.kind(), - io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted - ) - } - - fn is_fatal_error(e: &io::Error) -> bool { - matches!( - e.kind(), - io::ErrorKind::UnexpectedEof - | io::ErrorKind::ConnectionReset - | io::ErrorKind::ConnectionAborted - | io::ErrorKind::BrokenPipe - ) - } - - async fn read_into(&self, stream: &mut W, buf: &mut BytesMut) -> io::Result> - where - W: tokio::io::AsyncRead + Unpin, - { - use tokio::{ - io::AsyncReadExt, - time::{Duration, timeout}, - }; - - const READ_TIMEOUT: Duration = Duration::from_millis(100); - - match timeout(READ_TIMEOUT, stream.read_buf(buf)).await { - Ok(Ok(n)) => Ok(Some(n)), - Ok(Err(e)) => Err(e), - Err(_) => Ok(None), - } - } - - async fn handle_frame( - &self, - stream: &mut W, - frame: &[u8], - deser_failures: &mut u32, - routes: &HashMap>, - ) -> io::Result<()> - where - W: tokio::io::AsyncWrite + Unpin, - { - // Parse the frame first; routing is handled below to avoid duplicating - // logic on the success path. - crate::metrics::inc_frames(crate::metrics::Direction::Inbound); - let (env, _) = match self.parse_envelope(frame) { - Ok(result) => { - *deser_failures = 0; - result - } - Err(e) => { - *deser_failures += 1; - tracing::warn!(correlation_id = ?None::, error = ?e, "failed to deserialize message"); - crate::metrics::inc_deser_errors(); - if *deser_failures >= MAX_DESER_FAILURES { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "too many deserialization failures", - )); - } - return Ok(()); - } - }; - - if let Some(service) = routes.get(&env.id) { - let request = ServiceRequest::new(env.payload, env.correlation_id); - match service.call(request).await { - Ok(resp) => { - let parts = PacketParts::new(env.id, resp.correlation_id(), resp.into_inner()) - .inherit_correlation(env.correlation_id); - let correlation_id = parts.correlation_id(); - let response = Envelope::from_parts(parts); - if let Err(e) = self.send_response(stream, &response).await { - tracing::warn!( - id = env.id, - correlation_id = ?correlation_id, - error = ?e, - "failed to send response", - ); - crate::metrics::inc_handler_errors(); - } - } - Err(e) => { - tracing::warn!(id = env.id, correlation_id = ?env.correlation_id, error = ?e, "handler error"); - crate::metrics::inc_handler_errors(); - } - } - } else { - tracing::warn!(id = env.id, correlation_id = ?env.correlation_id, "no handler for message id"); - crate::metrics::inc_handler_errors(); - } - - Ok(()) - } -} diff --git a/src/app/builder.rs b/src/app/builder.rs new file mode 100644 index 00000000..9f22bc23 --- /dev/null +++ b/src/app/builder.rs @@ -0,0 +1,385 @@ +//! Application builder configuring routes and middleware. +//! +//! This module defines [`WireframeApp`], an Actix-inspired builder for +//! managing connection state, routing, and middleware in a `WireframeServer`. +//! It exposes convenience methods to register handlers and lifecycle hooks. + +use std::{ + any::{Any, TypeId}, + boxed::Box, + collections::HashMap, + future::Future, + pin::Pin, + sync::Arc, +}; + +use tokio::{io, sync::mpsc}; + +use super::{ + envelope::{Envelope, Packet}, + error::{Result, WireframeError}, +}; +use crate::{ + frame::{FrameProcessor, LengthFormat, LengthPrefixedProcessor}, + hooks::{ProtocolHooks, WireframeProtocol}, + middleware::{HandlerService, Transform}, + serializer::{BincodeSerializer, Serializer}, +}; + +type BoxedFrameProcessor = + Box, Error = io::Error> + Send + Sync>; + +/// Callback invoked when a connection is established. +/// +/// # Examples +/// +/// ```no_run +/// use std::sync::Arc; +/// +/// use wireframe::app::ConnectionSetup; +/// +/// let setup: Arc> = Arc::new(|| { +/// Box::pin(async { +/// // Perform authentication and return connection state +/// String::from("hello") +/// }) +/// }); +/// ``` +pub type ConnectionSetup = dyn Fn() -> Pin + Send>> + Send + Sync; + +/// Callback invoked when a connection is closed. +/// +/// # Examples +/// +/// ```no_run +/// use std::sync::Arc; +/// +/// use wireframe::app::ConnectionTeardown; +/// +/// let teardown: Arc> = Arc::new(|state| { +/// Box::pin(async move { +/// println!("Dropping {state}"); +/// }) +/// }); +/// ``` +pub type ConnectionTeardown = + dyn Fn(C) -> Pin + Send>> + Send + Sync; + +/// Configures routing and middleware for a `WireframeServer`. +/// +/// The builder stores registered routes, services, and middleware +/// without enforcing an ordering. Methods return [`Result`] so +/// registrations can be chained ergonomically. +pub struct WireframeApp< + S: Serializer + Send + Sync = BincodeSerializer, + C: Send + 'static = (), + E: Packet = Envelope, +> { + pub(super) routes: HashMap>, + pub(super) services: Vec>, + pub(super) middleware: Vec, Output = HandlerService>>>, + pub(super) frame_processor: BoxedFrameProcessor, + pub(super) serializer: S, + pub(super) app_data: HashMap>, + pub(super) on_connect: Option>>, + pub(super) on_disconnect: Option>>, + pub(super) protocol: Option, ProtocolError = ()>>>, + pub(super) push_dlq: Option>>, +} + +/// Alias for asynchronous route handlers. +/// +/// A `Handler` is an `Arc` to a function returning a [`Future`], enabling +/// asynchronous execution of message handlers. +pub type Handler = Arc Pin + Send>> + Send + Sync>; + +/// Trait representing middleware components. +pub trait Middleware: + Transform, Output = HandlerService> + Send + Sync +{ +} + +impl Middleware for T where + T: Transform, Output = HandlerService> + Send + Sync +{ +} + +impl Default for WireframeApp +where + S: Serializer + Default + Send + Sync, + C: Send + 'static, + E: Packet, +{ + /// + /// Initializes empty routes, services, middleware, and application data. + /// Sets the default frame processor and serializer, with no connection + /// lifecycle hooks. + fn default() -> Self { + Self { + routes: HashMap::new(), + services: Vec::new(), + middleware: Vec::new(), + frame_processor: Box::new(LengthPrefixedProcessor::new(LengthFormat::default())), + serializer: S::default(), + app_data: HashMap::new(), + on_connect: None, + on_disconnect: None, + protocol: None, + push_dlq: None, + } + } +} + +impl WireframeApp +where + E: Packet, +{ + /// Construct a new empty application builder. + /// + /// # Errors + /// + /// This function currently never returns an error but uses [`Result`] for + /// forward compatibility. + /// + /// # Examples + /// + /// ``` + /// use wireframe::app::{Packet, PacketParts, WireframeApp}; + /// + /// #[derive(bincode::Encode, bincode::BorrowDecode)] + /// struct MyEnv { + /// id: u32, + /// correlation_id: Option, + /// data: Vec, + /// } + /// + /// impl Packet for MyEnv { + /// fn id(&self) -> u32 { self.id } + /// fn correlation_id(&self) -> Option { self.correlation_id } + /// fn into_parts(self) -> PacketParts { + /// PacketParts::new(self.id, self.correlation_id, self.data) + /// } + /// fn from_parts(parts: PacketParts) -> Self { + /// Self { + /// id: parts.id(), + /// correlation_id: parts.correlation_id(), + /// data: parts.payload(), + /// } + /// } + /// } + /// + /// let app = WireframeApp::<_, _, MyEnv>::new().expect("failed to create app"); + /// ``` + pub fn new() -> Result { Ok(Self::default()) } + + /// Construct a new application builder using a custom envelope type. + /// + /// Deprecated: call [`WireframeApp::new`] with explicit envelope type + /// parameters. + /// + /// # Errors + /// + /// This function currently never returns an error but uses [`Result`] for + /// forward compatibility. + #[deprecated(note = "use `WireframeApp::<_, _, E>::new()` instead")] + pub fn new_with_envelope() -> Result { Self::new() } +} + +impl WireframeApp +where + S: Serializer + Send + Sync, + C: Send + 'static, + E: Packet, +{ + /// Register a route that maps `id` to `handler`. + /// + /// # Errors + /// + /// Returns [`WireframeError::DuplicateRoute`] if a handler for `id` + /// has already been registered. + pub fn route(mut self, id: u32, handler: Handler) -> Result { + if self.routes.contains_key(&id) { + return Err(WireframeError::DuplicateRoute(id)); + } + self.routes.insert(id, handler); + Ok(self) + } + + /// Register a handler discovered by attribute macros or other means. + /// + /// # Errors + /// + /// This function always succeeds currently but uses [`Result`] for + /// consistency with other builder methods. + pub fn service(mut self, handler: Handler) -> Result { + self.services.push(handler); + Ok(self) + } + + /// Store a shared state value accessible to request extractors. + /// + /// The value can later be retrieved using [`crate::extractor::SharedState`]. Registering + /// another value of the same type overwrites the previous one. + #[must_use] + pub fn app_data(mut self, state: T) -> Self + where + T: Send + Sync + 'static, + { + self.app_data.insert( + TypeId::of::(), + Arc::new(state) as Arc, + ); + self + } + + /// Add a middleware component to the processing pipeline. + /// + /// # Errors + /// + /// This function currently always succeeds. + pub fn wrap(mut self, mw: M) -> Result + where + M: Transform, Output = HandlerService> + Send + Sync + 'static, + { + self.middleware.push(Box::new(mw)); + Ok(self) + } + + /// Register a callback invoked when a new connection is established. + /// + /// The callback can perform authentication or other setup tasks and + /// returns connection-specific state stored for the connection's + /// lifetime. + /// + /// # Type Parameters + /// + /// This method changes the connection state type parameter from `C` to `C2`. + /// This means that any subsequent builder methods will operate on the new connection state type + /// `C2`. Be aware of this type transition when chaining builder methods. + /// + /// # Errors + /// + /// This function always succeeds currently but uses [`Result`] for + /// consistency with other builder methods. + pub fn on_connection_setup(self, f: F) -> Result> + where + F: Fn() -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, + C2: Send + 'static, + { + Ok(WireframeApp { + routes: self.routes, + services: self.services, + 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, + protocol: self.protocol, + push_dlq: self.push_dlq, + }) + } + + /// Register a callback invoked when a connection is closed. + /// + /// The callback receives the connection state produced by + /// [`on_connection_setup`](Self::on_connection_setup). + /// + /// # Errors + /// + /// This function always succeeds currently but uses [`Result`] for + /// consistency with other builder methods. + pub fn on_connection_teardown(mut self, f: F) -> Result + where + F: Fn(C) -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, + { + self.on_disconnect = Some(Arc::new(move |c| Box::pin(f(c)))); + Ok(self) + } + + /// Install a [`WireframeProtocol`] implementation. + /// + /// The protocol defines hooks for connection setup, frame modification, and + /// command completion. It is wrapped in an [`Arc`] and stored for later use + /// by the connection actor. + #[must_use] + pub fn with_protocol

(mut self, protocol: P) -> Self + where + P: WireframeProtocol, ProtocolError = ()> + 'static, + { + self.protocol = Some(Arc::new(protocol)); + self + } + + /// Configure a Dead Letter Queue for dropped push frames. + /// + /// ```rust,no_run + /// use tokio::sync::mpsc; + /// use wireframe::app::WireframeApp; + /// + /// # fn build() -> WireframeApp { WireframeApp::new().unwrap() } + /// # fn main() { + /// let (tx, _rx) = mpsc::channel(16); + /// let app = build().with_push_dlq(tx); + /// # let _ = app; + /// # } + /// ``` + #[must_use] + pub fn with_push_dlq(mut self, dlq: mpsc::Sender>) -> Self { + self.push_dlq = Some(dlq); + self + } + + /// Get a clone of the configured protocol, if any. + /// + /// Returns `None` if no protocol was installed via [`with_protocol`](Self::with_protocol). + #[must_use] + pub fn protocol( + &self, + ) -> Option, ProtocolError = ()>>> { + self.protocol.as_ref().map(Arc::clone) + } + + /// Return protocol hooks derived from the installed protocol. + /// + /// If no protocol is installed, returns default (no-op) hooks. + #[must_use] + pub fn protocol_hooks(&self) -> ProtocolHooks, ()> { + self.protocol + .as_ref() + .map(|p| ProtocolHooks::from_protocol(&Arc::clone(p))) + .unwrap_or_default() + } + + /// Set the frame processor used for encoding and decoding frames. + #[must_use] + pub fn frame_processor

(mut self, processor: P) -> Self + where + P: FrameProcessor, Error = io::Error> + Send + Sync + 'static, + { + self.frame_processor = Box::new(processor); + self + } + + /// Replace the serializer used for messages. + #[must_use] + pub fn serializer(self, serializer: Ser) -> WireframeApp + where + Ser: Serializer + Send + Sync, + { + WireframeApp { + routes: self.routes, + services: self.services, + middleware: self.middleware, + frame_processor: self.frame_processor, + serializer, + app_data: self.app_data, + on_connect: self.on_connect, + on_disconnect: self.on_disconnect, + protocol: self.protocol, + push_dlq: self.push_dlq, + } + } +} diff --git a/src/app/connection.rs b/src/app/connection.rs new file mode 100644 index 00000000..12f15e20 --- /dev/null +++ b/src/app/connection.rs @@ -0,0 +1,258 @@ +//! Connection handling and response utilities for `WireframeApp`. + +use std::collections::HashMap; + +use bytes::BytesMut; +use tokio::{ + io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + time::{Duration, timeout}, +}; + +use super::{ + builder::WireframeApp, + envelope::{Envelope, Packet, PacketParts}, + error::SendError, +}; +use crate::{ + frame::FrameMetadata, + message::Message, + middleware::{HandlerService, Service, ServiceRequest}, + serializer::Serializer, +}; + +/// Number of idle polls before terminating a connection. +const MAX_IDLE_POLLS: u32 = 50; // ~5s with 100ms timeout +/// Maximum consecutive deserialization failures before closing a connection. +const MAX_DESER_FAILURES: u32 = 10; + +impl WireframeApp +where + S: Serializer + Send + Sync, + C: Send + 'static, + E: Packet, +{ + /// Serialize `msg` and write it to `stream` using the frame processor. + /// + /// # Errors + /// + /// Returns a [`SendError`] if serialization or writing fails. + pub async fn send_response( + &self, + stream: &mut W, + msg: &M, + ) -> std::result::Result<(), SendError> + where + W: AsyncWrite + Unpin, + M: Message, + { + let bytes = self + .serializer + .serialize(msg) + .map_err(SendError::Serialize)?; + let mut framed = BytesMut::with_capacity(4 + bytes.len()); + self.frame_processor + .encode(&bytes, &mut framed) + .map_err(SendError::Io)?; + stream.write_all(&framed).await.map_err(SendError::Io)?; + stream.flush().await.map_err(SendError::Io) + } +} + +impl WireframeApp +where + S: Serializer + FrameMetadata + Send + Sync, + C: Send + 'static, + E: Packet, +{ + /// Try parsing the frame using [`FrameMetadata::parse`], falling back to + /// full deserialization on failure. + fn parse_envelope( + &self, + frame: &[u8], + ) -> std::result::Result<(Envelope, usize), Box> { + self.serializer + .parse(frame) + .map_err(|e| Box::new(e) as Box) + .or_else(|_| self.serializer.deserialize::(frame)) + } + + /// Handle an accepted connection. + /// + /// This placeholder immediately closes the connection after the + /// preamble phase. A warning is logged so tests and callers are + /// aware of the current limitation. + pub async fn handle_connection(&self, mut stream: W) + where + W: AsyncRead + AsyncWrite + Send + Unpin + 'static, + { + let state = if let Some(setup) = &self.on_connect { + Some((setup)().await) + } else { + None + }; + + let routes = self.build_chains().await; + + if let Err(e) = self.process_stream(&mut stream, &routes).await { + tracing::warn!(correlation_id = ?None::, error = ?e, "connection terminated with error"); + } + + if let (Some(teardown), Some(state)) = (&self.on_disconnect, state) { + teardown(state).await; + } + } + + async fn build_chains(&self) -> HashMap> { + let mut routes = HashMap::new(); + for (&id, handler) in &self.routes { + let mut service = HandlerService::new(id, handler.clone()); + for mw in self.middleware.iter().rev() { + service = mw.transform(service).await; + } + routes.insert(id, service); + } + routes + } + + async fn process_stream( + &self, + stream: &mut W, + routes: &HashMap>, + ) -> io::Result<()> + where + W: AsyncRead + AsyncWrite + Unpin, + { + let mut buf = BytesMut::with_capacity(1024); + let mut idle = 0u32; + let mut deser_failures = 0u32; + + loop { + if let Some(frame) = self.frame_processor.decode(&mut buf)? { + self.handle_frame(stream, &frame, &mut deser_failures, routes) + .await?; + idle = 0; + continue; + } + + if self.read_and_update(stream, &mut buf, &mut idle).await? { + break; + } + } + + Ok(()) + } + + async fn read_and_update( + &self, + stream: &mut W, + buf: &mut BytesMut, + idle: &mut u32, + ) -> io::Result + where + W: AsyncRead + AsyncWrite + Unpin, + { + match self.read_into(stream, buf).await { + Ok(Some(0)) => Ok(true), + Ok(Some(_)) => { + *idle = 0; + Ok(false) + } + Ok(None) => { + *idle += 1; + Ok(*idle >= MAX_IDLE_POLLS) + } + Err(e) if Self::is_transient_error(&e) => Ok(false), + Err(e) if Self::is_fatal_error(&e) => Ok(true), + Err(e) => Err(e), + } + } + + fn is_transient_error(e: &io::Error) -> bool { + matches!( + e.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted + ) + } + + fn is_fatal_error(e: &io::Error) -> bool { + matches!( + e.kind(), + io::ErrorKind::UnexpectedEof + | io::ErrorKind::ConnectionReset + | io::ErrorKind::ConnectionAborted + | io::ErrorKind::BrokenPipe + ) + } + + async fn read_into(&self, stream: &mut W, buf: &mut BytesMut) -> io::Result> + where + W: AsyncRead + Unpin, + { + match timeout(Duration::from_millis(100), stream.read_buf(buf)).await { + Ok(Ok(n)) => Ok(Some(n)), + Ok(Err(e)) => Err(e), + Err(_) => Ok(None), + } + } + + async fn handle_frame( + &self, + stream: &mut W, + frame: &[u8], + deser_failures: &mut u32, + routes: &HashMap>, + ) -> io::Result<()> + where + W: AsyncWrite + Unpin, + { + crate::metrics::inc_frames(crate::metrics::Direction::Inbound); + let (env, _) = match self.parse_envelope(frame) { + Ok(result) => { + *deser_failures = 0; + result + } + Err(e) => { + *deser_failures += 1; + tracing::warn!(correlation_id = ?None::, error = ?e, "failed to deserialize message"); + crate::metrics::inc_deser_errors(); + if *deser_failures >= MAX_DESER_FAILURES { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "too many deserialization failures", + )); + } + return Ok(()); + } + }; + + if let Some(service) = routes.get(&env.id) { + let request = ServiceRequest::new(env.payload, env.correlation_id); + match service.call(request).await { + Ok(resp) => { + let parts = PacketParts::new(env.id, resp.correlation_id(), resp.into_inner()) + .inherit_correlation(env.correlation_id); + let correlation_id = parts.correlation_id(); + let response = Envelope::from_parts(parts); + if let Err(e) = self.send_response(stream, &response).await { + tracing::warn!( + id = env.id, + correlation_id = ?correlation_id, + error = ?e, + "failed to send response", + ); + crate::metrics::inc_handler_errors(); + } + } + Err(e) => { + tracing::warn!(id = env.id, correlation_id = ?env.correlation_id, error = ?e, "handler error"); + crate::metrics::inc_handler_errors(); + } + } + } else { + tracing::warn!(id = env.id, correlation_id = ?env.correlation_id, "no handler for message id"); + crate::metrics::inc_handler_errors(); + } + + Ok(()) + } +} diff --git a/src/app/envelope.rs b/src/app/envelope.rs new file mode 100644 index 00000000..f76e504d --- /dev/null +++ b/src/app/envelope.rs @@ -0,0 +1,166 @@ +//! Packet abstraction and envelope types. +//! +//! This module defines the [`Packet`] trait along with [`Envelope`] and +//! [`PacketParts`] used to decompose and reassemble messages. + +use crate::message::Message; + +/// Envelope-like type used to wrap incoming and outgoing messages. +/// +/// Custom envelope types must implement this trait so [`WireframeApp`] can +/// route messages and construct responses. +/// +/// # Example +/// +/// ``` +/// use wireframe::{ +/// app::{Packet, PacketParts}, +/// message::Message, +/// }; +/// +/// #[derive(bincode::Decode, bincode::Encode)] +/// struct CustomEnvelope { +/// id: u32, +/// payload: Vec, +/// timestamp: u64, +/// } +/// +/// impl Packet for CustomEnvelope { +/// fn id(&self) -> u32 { self.id } +/// +/// fn correlation_id(&self) -> Option { None } +/// +/// fn into_parts(self) -> PacketParts { PacketParts::new(self.id, None, self.payload) } +/// +/// fn from_parts(parts: PacketParts) -> Self { +/// Self { +/// id: parts.id(), +/// payload: parts.payload(), +/// timestamp: 0, +/// } +/// } +/// } +/// ``` +pub trait Packet: Message + Send + Sync + 'static { + /// Return the message identifier used for routing. + fn id(&self) -> u32; + + /// Return the correlation identifier tying this frame to a request. + fn correlation_id(&self) -> Option; + + /// Consume the packet and return its identifier, correlation id and payload bytes. + fn into_parts(self) -> PacketParts; + + /// Construct a new packet from raw parts. + fn from_parts(parts: PacketParts) -> Self; +} + +/// Component values extracted from or used to build a [`Packet`]. +#[derive(Debug)] +pub struct PacketParts { + id: u32, + correlation_id: Option, + payload: Vec, +} + +/// Basic envelope type used by [`WireframeApp::handle_connection`]. +/// +/// Incoming frames are deserialized into an `Envelope` containing the +/// message identifier and raw payload bytes. +#[derive(bincode::Decode, bincode::Encode, Debug)] +pub struct Envelope { + pub(crate) id: u32, + pub(crate) correlation_id: Option, + pub(crate) payload: Vec, +} + +impl Envelope { + /// Create a new [`Envelope`] with the provided identifiers and payload. + #[must_use] + pub fn new(id: u32, correlation_id: Option, payload: Vec) -> Self { + Self { + id, + correlation_id, + payload, + } + } +} + +impl Packet for Envelope { + #[inline] + fn id(&self) -> u32 { self.id } + + #[inline] + fn correlation_id(&self) -> Option { self.correlation_id } + + fn into_parts(self) -> PacketParts { self.into() } + + fn from_parts(parts: PacketParts) -> Self { parts.into() } +} + +impl PacketParts { + /// Construct a new set of packet parts. + #[must_use] + pub fn new(id: u32, correlation_id: Option, payload: Vec) -> Self { + Self { + id, + correlation_id, + payload, + } + } + + #[must_use] + pub const fn id(&self) -> u32 { self.id } + + #[must_use] + pub const fn correlation_id(&self) -> Option { self.correlation_id } + + #[must_use] + pub fn payload(self) -> Vec { self.payload } + + /// Ensure a correlation identifier is present, inheriting from `source` if missing. + /// + /// # Examples + /// ``` + /// use wireframe::app::PacketParts; + /// // Inherit when missing + /// let parts = PacketParts::new(1, None, vec![]).inherit_correlation(Some(42)); + /// assert_eq!(parts.correlation_id(), Some(42)); + /// + /// // Overwrite mismatched value + /// let parts = PacketParts::new(1, Some(7), vec![]).inherit_correlation(Some(8)); + /// assert_eq!(parts.correlation_id(), Some(8)); + /// ``` + #[must_use] + pub fn inherit_correlation(mut self, source: Option) -> Self { + match (self.correlation_id, source) { + (None, cid) => self.correlation_id = cid, + (Some(cid), Some(src)) if cid != src => { + tracing::warn!( + id = self.id, + expected = src, + found = cid, + "mismatched correlation id in response", + ); + // Overwrite with the source correlation ID to ensure downstream + // consistency. + self.correlation_id = Some(src); + } + _ => {} + } + self + } +} + +impl From for PacketParts { + fn from(e: Envelope) -> Self { PacketParts::new(e.id, e.correlation_id, e.payload) } +} + +impl From for Envelope { + fn from(p: PacketParts) -> Self { + let id = p.id(); + let correlation_id = p.correlation_id(); + let payload = p.payload(); + Envelope::new(id, correlation_id, payload) + } +} diff --git a/src/app/error.rs b/src/app/error.rs new file mode 100644 index 00000000..315ce024 --- /dev/null +++ b/src/app/error.rs @@ -0,0 +1,44 @@ +//! Error types for application setup and messaging. + +use tokio::io; + +/// Top-level error type for application setup. +#[derive(Debug)] +pub enum WireframeError { + /// A route with the provided identifier was already registered. + DuplicateRoute(u32), +} + +/// Errors produced when sending a handler response over a stream. +#[derive(Debug)] +pub enum SendError { + /// Serialization failed. + Serialize(Box), + /// Writing to the stream failed. + Io(io::Error), +} + +impl std::fmt::Display for SendError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SendError::Serialize(e) => write!(f, "serialization error: {e}"), + SendError::Io(e) => write!(f, "I/O error: {e}"), + } + } +} + +impl std::error::Error for SendError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + SendError::Serialize(e) => Some(&**e), + SendError::Io(e) => Some(e), + } + } +} + +impl From for SendError { + fn from(e: io::Error) -> Self { SendError::Io(e) } +} + +/// Result type used throughout the builder API. +pub type Result = std::result::Result; diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 00000000..caa92ff2 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,10 @@ +//! Application builder and related types. + +mod builder; +mod connection; +mod envelope; +mod error; + +pub use builder::{ConnectionSetup, ConnectionTeardown, Handler, Middleware, WireframeApp}; +pub use envelope::{Envelope, Packet, PacketParts}; +pub use error::{Result, SendError, WireframeError}; From 8691db340e1a63a9c5517cfca9e2603b9a01493c Mon Sep 17 00:00:00 2001 From: Leynos Date: Tue, 26 Aug 2025 16:09:56 +0100 Subject: [PATCH 02/15] Cache routes and simplify connection framing --- Cargo.toml | 2 +- examples/echo.rs | 3 +- examples/metadata_routing.rs | 4 +- examples/packet_enum.rs | 3 +- examples/ping_pong.rs | 4 +- src/app/builder.rs | 105 ++++++++++++++++------------ src/app/connection.rs | 131 ++++++++++++----------------------- tests/common/mod.rs | 10 ++- tests/lifecycle.rs | 6 +- tests/metadata.rs | 4 +- tests/middleware_order.rs | 2 +- tests/response.rs | 6 +- tests/routes.rs | 8 +-- tests/wireframe_protocol.rs | 5 +- tests/world.rs | 3 +- 15 files changed, 139 insertions(+), 157 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d547972..bd16be1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ tokio = { version = "1.46.1", default-features = false, features = [ "io-util", "test-util", ] } -tokio-util = { version = "0.7.16", features = ["rt"] } +tokio-util = { version = "0.7.16", features = ["rt", "codec"] } futures = "0.3.31" async-trait = "0.1.88" bytes = "1.10.1" diff --git a/examples/echo.rs b/examples/echo.rs index 89e4f3b9..43c2e795 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -5,13 +5,14 @@ use wireframe::{ app::{Envelope, WireframeApp}, + serializer::BincodeSerializer, server::{ServerError, WireframeServer}, }; #[tokio::main] async fn main() -> Result<(), ServerError> { let factory = || { - WireframeApp::new() + WireframeApp::::new() .expect("failed to create WireframeApp") .route( 1, diff --git a/examples/metadata_routing.rs b/examples/metadata_routing.rs index aae264aa..9d9bedaf 100644 --- a/examples/metadata_routing.rs +++ b/examples/metadata_routing.rs @@ -11,7 +11,7 @@ use wireframe::{ app::{Envelope, WireframeApp}, frame::{FrameMetadata, FrameProcessor, LengthPrefixedProcessor}, message::Message, - serializer::Serializer, + serializer::{BincodeSerializer, Serializer}, }; /// Frame format with a two-byte id, one-byte flags, and bincode payload. @@ -61,7 +61,7 @@ struct Ping; #[tokio::main] async fn main() -> io::Result<()> { - let app = WireframeApp::new() + let app = WireframeApp::::new() .expect("failed to create app") .frame_processor(LengthPrefixedProcessor::default()) .serializer(HeaderSerializer) diff --git a/examples/packet_enum.rs b/examples/packet_enum.rs index ff53fd8b..5c85fbb0 100644 --- a/examples/packet_enum.rs +++ b/examples/packet_enum.rs @@ -11,6 +11,7 @@ use wireframe::{ frame::{LengthFormat, LengthPrefixedProcessor}, message::Message, middleware::{HandlerService, Service, ServiceRequest, ServiceResponse, Transform}, + serializer::BincodeSerializer, server::{ServerError, WireframeServer}, }; @@ -78,7 +79,7 @@ fn handle_packet(_env: &Envelope) -> Pin + Send>> { #[tokio::main] async fn main() -> Result<(), ServerError> { let factory = || { - WireframeApp::new() + WireframeApp::::new() .expect("Failed to create WireframeApp") .frame_processor(LengthPrefixedProcessor::new(LengthFormat::u16_le())) .wrap(DecodeMiddleware) diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs index 5a4a5f5a..f5681043 100644 --- a/examples/ping_pong.rs +++ b/examples/ping_pong.rs @@ -130,8 +130,8 @@ impl Transform> for Logging { } } -fn build_app() -> AppResult { - WireframeApp::new()? +fn build_app() -> AppResult> { + WireframeApp::::new()? .serializer(BincodeSerializer) .route(PING_ID, Arc::new(|_: &Envelope| Box::pin(ping_handler())))? .wrap(PongMiddleware)? diff --git a/src/app/builder.rs b/src/app/builder.rs index 9f22bc23..8d7e8c68 100644 --- a/src/app/builder.rs +++ b/src/app/builder.rs @@ -1,5 +1,4 @@ //! Application builder configuring routes and middleware. -//! //! This module defines [`WireframeApp`], an Actix-inspired builder for //! managing connection state, routing, and middleware in a `WireframeServer`. //! It exposes convenience methods to register handlers and lifecycle hooks. @@ -13,7 +12,10 @@ use std::{ sync::Arc, }; -use tokio::{io, sync::mpsc}; +use tokio::{ + io, + sync::{OnceCell, mpsc}, +}; use super::{ envelope::{Envelope, Packet}, @@ -25,7 +27,6 @@ use crate::{ middleware::{HandlerService, Transform}, serializer::{BincodeSerializer, Serializer}, }; - type BoxedFrameProcessor = Box, Error = io::Error> + Send + Sync>; @@ -75,7 +76,8 @@ pub struct WireframeApp< C: Send + 'static = (), E: Packet = Envelope, > { - pub(super) routes: HashMap>, + pub(super) handlers: HashMap>, + pub(super) routes: OnceCell>>>, pub(super) services: Vec>, pub(super) middleware: Vec, Output = HandlerService>>>, pub(super) frame_processor: BoxedFrameProcessor, @@ -85,6 +87,8 @@ pub struct WireframeApp< pub(super) on_disconnect: Option>>, pub(super) protocol: Option, ProtocolError = ()>>>, pub(super) push_dlq: Option>>, + pub(super) buffer_capacity: usize, + pub(super) read_timeout_ms: u64, } /// Alias for asynchronous route handlers. @@ -116,7 +120,8 @@ where /// lifecycle hooks. fn default() -> Self { Self { - routes: HashMap::new(), + handlers: HashMap::new(), + routes: OnceCell::new(), services: Vec::new(), middleware: Vec::new(), frame_processor: Box::new(LengthPrefixedProcessor::new(LengthFormat::default())), @@ -126,12 +131,16 @@ where on_disconnect: None, protocol: None, push_dlq: None, + buffer_capacity: 1024, + read_timeout_ms: 100, } } } -impl WireframeApp +impl WireframeApp where + S: Serializer + Default + Send + Sync, + C: Send + 'static, E: Packet, { /// Construct a new empty application builder. @@ -144,31 +153,9 @@ where /// # Examples /// /// ``` - /// use wireframe::app::{Packet, PacketParts, WireframeApp}; - /// - /// #[derive(bincode::Encode, bincode::BorrowDecode)] - /// struct MyEnv { - /// id: u32, - /// correlation_id: Option, - /// data: Vec, - /// } - /// - /// impl Packet for MyEnv { - /// fn id(&self) -> u32 { self.id } - /// fn correlation_id(&self) -> Option { self.correlation_id } - /// fn into_parts(self) -> PacketParts { - /// PacketParts::new(self.id, self.correlation_id, self.data) - /// } - /// fn from_parts(parts: PacketParts) -> Self { - /// Self { - /// id: parts.id(), - /// correlation_id: parts.correlation_id(), - /// data: parts.payload(), - /// } - /// } - /// } - /// - /// let app = WireframeApp::<_, _, MyEnv>::new().expect("failed to create app"); + /// use wireframe::app::WireframeApp; + /// let app = WireframeApp::<_, _, wireframe::app::Envelope>::new().unwrap(); + /// assert!(app.protocol().is_none()); /// ``` pub fn new() -> Result { Ok(Self::default()) } @@ -181,7 +168,7 @@ where /// /// This function currently never returns an error but uses [`Result`] for /// forward compatibility. - #[deprecated(note = "use `WireframeApp::<_, _, E>::new()` instead")] + #[deprecated(note = "use `WireframeApp::new()` instead")] pub fn new_with_envelope() -> Result { Self::new() } } @@ -198,10 +185,11 @@ where /// Returns [`WireframeError::DuplicateRoute`] if a handler for `id` /// has already been registered. pub fn route(mut self, id: u32, handler: Handler) -> Result { - if self.routes.contains_key(&id) { + if self.handlers.contains_key(&id) { return Err(WireframeError::DuplicateRoute(id)); } - self.routes.insert(id, handler); + self.handlers.insert(id, handler); + self.routes = OnceCell::new(); Ok(self) } @@ -213,6 +201,7 @@ where /// consistency with other builder methods. pub fn service(mut self, handler: Handler) -> Result { self.services.push(handler); + self.routes = OnceCell::new(); Ok(self) } @@ -242,6 +231,7 @@ where M: Transform, Output = HandlerService> + Send + Sync + 'static, { self.middleware.push(Box::new(mw)); + self.routes = OnceCell::new(); Ok(self) } @@ -268,7 +258,8 @@ where C2: Send + 'static, { Ok(WireframeApp { - routes: self.routes, + handlers: self.handlers, + routes: OnceCell::new(), services: self.services, middleware: self.middleware, frame_processor: self.frame_processor, @@ -278,6 +269,8 @@ where on_disconnect: None, protocol: self.protocol, push_dlq: self.push_dlq, + buffer_capacity: self.buffer_capacity, + read_timeout_ms: self.read_timeout_ms, }) } @@ -305,12 +298,14 @@ where /// command completion. It is wrapped in an [`Arc`] and stored for later use /// by the connection actor. #[must_use] - pub fn with_protocol

(mut self, protocol: P) -> Self + pub fn with_protocol

(self, protocol: P) -> Self where P: WireframeProtocol, ProtocolError = ()> + 'static, { - self.protocol = Some(Arc::new(protocol)); - self + WireframeApp { + protocol: Some(Arc::new(protocol)), + ..self + } } /// Configure a Dead Letter Queue for dropped push frames. @@ -327,9 +322,11 @@ where /// # } /// ``` #[must_use] - pub fn with_push_dlq(mut self, dlq: mpsc::Sender>) -> Self { - self.push_dlq = Some(dlq); - self + pub fn with_push_dlq(self, dlq: mpsc::Sender>) -> Self { + WireframeApp { + push_dlq: Some(dlq), + ..self + } } /// Get a clone of the configured protocol, if any. @@ -355,12 +352,14 @@ where /// Set the frame processor used for encoding and decoding frames. #[must_use] - pub fn frame_processor

(mut self, processor: P) -> Self + pub fn frame_processor

(self, processor: P) -> Self where P: FrameProcessor, Error = io::Error> + Send + Sync + 'static, { - self.frame_processor = Box::new(processor); - self + WireframeApp { + frame_processor: Box::new(processor), + ..self + } } /// Replace the serializer used for messages. @@ -370,7 +369,8 @@ where Ser: Serializer + Send + Sync, { WireframeApp { - routes: self.routes, + handlers: self.handlers, + routes: OnceCell::new(), services: self.services, middleware: self.middleware, frame_processor: self.frame_processor, @@ -380,6 +380,21 @@ where on_disconnect: self.on_disconnect, protocol: self.protocol, push_dlq: self.push_dlq, + buffer_capacity: self.buffer_capacity, + read_timeout_ms: self.read_timeout_ms, } } + + /// Set the initial buffer capacity for framed reads. + #[must_use] + pub fn buffer_capacity(mut self, capacity: usize) -> Self { + self.buffer_capacity = capacity; + self + } + /// Configure the read timeout in milliseconds. + #[must_use] + pub fn read_timeout_ms(mut self, timeout_ms: u64) -> Self { + self.read_timeout_ms = timeout_ms; + self + } } diff --git a/src/app/connection.rs b/src/app/connection.rs index 12f15e20..7e7e060d 100644 --- a/src/app/connection.rs +++ b/src/app/connection.rs @@ -1,12 +1,14 @@ //! Connection handling and response utilities for `WireframeApp`. -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use bytes::BytesMut; +use futures::{SinkExt, StreamExt}; use tokio::{ - io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + io::{self, AsyncRead, AsyncWrite, AsyncWriteExt}, time::{Duration, timeout}, }; +use tokio_util::codec::{Framed, LengthDelimitedCodec}; use super::{ builder::WireframeApp, @@ -20,8 +22,6 @@ use crate::{ serializer::Serializer, }; -/// Number of idle polls before terminating a connection. -const MAX_IDLE_POLLS: u32 = 50; // ~5s with 100ms timeout /// Maximum consecutive deserialization failures before closing a connection. const MAX_DESER_FAILURES: u32 = 10; @@ -81,7 +81,7 @@ where /// This placeholder immediately closes the connection after the /// preamble phase. A warning is logged so tests and callers are /// aware of the current limitation. - pub async fn handle_connection(&self, mut stream: W) + pub async fn handle_connection(&self, stream: W) where W: AsyncRead + AsyncWrite + Send + Unpin + 'static, { @@ -91,9 +91,13 @@ where None }; - let routes = self.build_chains().await; + let routes = self + .routes + .get_or_init(|| async { Arc::new(self.build_chains().await) }) + .await + .clone(); - if let Err(e) = self.process_stream(&mut stream, &routes).await { + if let Err(e) = self.process_stream(stream, &routes).await { tracing::warn!(correlation_id = ?None::, error = ?e, "connection terminated with error"); } @@ -104,7 +108,7 @@ where async fn build_chains(&self) -> HashMap> { let mut routes = HashMap::new(); - for (&id, handler) in &self.routes { + for (&id, handler) in &self.handlers { let mut service = HandlerService::new(id, handler.clone()); for mw in self.middleware.iter().rev() { service = mw.transform(service).await; @@ -116,94 +120,36 @@ where async fn process_stream( &self, - stream: &mut W, - routes: &HashMap>, + stream: W, + routes: &Arc>>, ) -> io::Result<()> where W: AsyncRead + AsyncWrite + Unpin, { - let mut buf = BytesMut::with_capacity(1024); - let mut idle = 0u32; + let codec = LengthDelimitedCodec::builder().new_codec(); + let mut framed = Framed::new(stream, codec); + framed.read_buffer_mut().reserve(self.buffer_capacity); let mut deser_failures = 0u32; + let timeout_dur = Duration::from_millis(self.read_timeout_ms); - loop { - if let Some(frame) = self.frame_processor.decode(&mut buf)? { - self.handle_frame(stream, &frame, &mut deser_failures, routes) - .await?; - idle = 0; - continue; - } - - if self.read_and_update(stream, &mut buf, &mut idle).await? { - break; - } + while let Ok(Some(frame)) = timeout(timeout_dur, framed.next()).await { + let buf = frame?; + self.handle_frame(&mut framed, buf.as_ref(), &mut deser_failures, routes) + .await?; } Ok(()) } - async fn read_and_update( - &self, - stream: &mut W, - buf: &mut BytesMut, - idle: &mut u32, - ) -> io::Result - where - W: AsyncRead + AsyncWrite + Unpin, - { - match self.read_into(stream, buf).await { - Ok(Some(0)) => Ok(true), - Ok(Some(_)) => { - *idle = 0; - Ok(false) - } - Ok(None) => { - *idle += 1; - Ok(*idle >= MAX_IDLE_POLLS) - } - Err(e) if Self::is_transient_error(&e) => Ok(false), - Err(e) if Self::is_fatal_error(&e) => Ok(true), - Err(e) => Err(e), - } - } - - fn is_transient_error(e: &io::Error) -> bool { - matches!( - e.kind(), - io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted - ) - } - - fn is_fatal_error(e: &io::Error) -> bool { - matches!( - e.kind(), - io::ErrorKind::UnexpectedEof - | io::ErrorKind::ConnectionReset - | io::ErrorKind::ConnectionAborted - | io::ErrorKind::BrokenPipe - ) - } - - async fn read_into(&self, stream: &mut W, buf: &mut BytesMut) -> io::Result> - where - W: AsyncRead + Unpin, - { - match timeout(Duration::from_millis(100), stream.read_buf(buf)).await { - Ok(Ok(n)) => Ok(Some(n)), - Ok(Err(e)) => Err(e), - Err(_) => Ok(None), - } - } - async fn handle_frame( &self, - stream: &mut W, + framed: &mut Framed, frame: &[u8], deser_failures: &mut u32, routes: &HashMap>, ) -> io::Result<()> where - W: AsyncWrite + Unpin, + W: AsyncRead + AsyncWrite + Unpin, { crate::metrics::inc_frames(crate::metrics::Direction::Inbound); let (env, _) = match self.parse_envelope(frame) { @@ -233,14 +179,27 @@ where .inherit_correlation(env.correlation_id); let correlation_id = parts.correlation_id(); let response = Envelope::from_parts(parts); - if let Err(e) = self.send_response(stream, &response).await { - tracing::warn!( - id = env.id, - correlation_id = ?correlation_id, - error = ?e, - "failed to send response", - ); - crate::metrics::inc_handler_errors(); + match self.serializer.serialize(&response) { + Ok(bytes) => { + if let Err(e) = framed.send(bytes.into()).await { + tracing::warn!( + id = env.id, + correlation_id = ?correlation_id, + error = ?e, + "failed to send response", + ); + crate::metrics::inc_handler_errors(); + } + } + Err(e) => { + tracing::warn!( + id = env.id, + correlation_id = ?correlation_id, + error = ?e, + "failed to serialize response", + ); + crate::metrics::inc_handler_errors(); + } } } Err(e) => { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 4405b3dc..d7e36482 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -13,13 +13,17 @@ pub fn unused_listener() -> StdTcpListener { } use rstest::fixture; -use wireframe::app::WireframeApp; +use wireframe::{ + app::{Envelope, WireframeApp}, + serializer::BincodeSerializer, +}; #[fixture] #[allow( unused_braces, reason = "rustc false positive for single line rstest fixtures" )] -pub fn factory() -> impl Fn() -> WireframeApp + Send + Sync + Clone + 'static { - || WireframeApp::new().expect("WireframeApp::new failed") +pub fn factory() +-> impl Fn() -> WireframeApp + Send + Sync + Clone + 'static { + || WireframeApp::::new().expect("WireframeApp::new failed") } diff --git a/tests/lifecycle.rs b/tests/lifecycle.rs index 6a00242f..ae0fecc0 100644 --- a/tests/lifecycle.rs +++ b/tests/lifecycle.rs @@ -49,7 +49,7 @@ where let setup_cb = call_counting_callback(setup, state); let teardown_cb = call_counting_callback(teardown, ()); - WireframeApp::<_, _, E>::new() + WireframeApp::::new() .expect("failed to create app") .on_connection_setup(move || setup_cb(())) .expect("setup callback") @@ -74,7 +74,7 @@ async fn setup_without_teardown_runs() { let setup_count = Arc::new(AtomicUsize::new(0)); let cb = call_counting_callback(&setup_count, ()); - let app = WireframeApp::<_, _, Envelope>::new() + let app = WireframeApp::::new() .expect("failed to create app") .on_connection_setup(move || cb(())) .expect("setup callback"); @@ -89,7 +89,7 @@ async fn teardown_without_setup_does_not_run() { let teardown_count = Arc::new(AtomicUsize::new(0)); let cb = call_counting_callback(&teardown_count, ()); - let app = WireframeApp::<_, _, Envelope>::new() + let app = WireframeApp::::new() .expect("failed to create app") .on_connection_teardown(cb) .expect("teardown callback"); diff --git a/tests/metadata.rs b/tests/metadata.rs index 45efcecb..b581977e 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -14,11 +14,11 @@ use wireframe::{ }; use wireframe_testing::{TestSerializer, drive_with_bincode}; -fn mock_wireframe_app_with_serializer(serializer: S) -> WireframeApp +fn mock_wireframe_app_with_serializer(serializer: S) -> WireframeApp where S: TestSerializer, { - WireframeApp::new() + WireframeApp::::new() .expect("failed to create app") .frame_processor(LengthPrefixedProcessor::default()) .serializer(serializer) diff --git a/tests/middleware_order.rs b/tests/middleware_order.rs index 6b66f55b..1e1df434 100644 --- a/tests/middleware_order.rs +++ b/tests/middleware_order.rs @@ -53,7 +53,7 @@ impl Transform> for TagMiddleware { #[tokio::test] async fn middleware_applied_in_reverse_order() { let handler: Handler = std::sync::Arc::new(|_env: &Envelope| Box::pin(async {})); - let app = WireframeApp::new() + let app = WireframeApp::::new() .expect("failed to create app") .route(1, handler) .expect("route registration failed") diff --git a/tests/response.rs b/tests/response.rs index 60b9de04..e13bcc84 100644 --- a/tests/response.rs +++ b/tests/response.rs @@ -39,7 +39,7 @@ impl<'de> bincode::BorrowDecode<'de, ()> for FailingResp { /// Tests that sending a response serialises and frames the data correctly, /// and that the response can be decoded and deserialised back to its original value asynchronously. async fn send_response_encodes_and_frames() { - let app = WireframeApp::<_, _, Envelope>::new() + let app = WireframeApp::::new() .expect("failed to create app") .frame_processor(LengthPrefixedProcessor::default()) .serializer(BincodeSerializer); @@ -131,7 +131,7 @@ fn custom_length_roundtrip( #[tokio::test] async fn send_response_propagates_write_error() { - let app = WireframeApp::<_, _, Envelope>::new() + let app = WireframeApp::::new() .expect("app creation failed") .frame_processor(LengthPrefixedProcessor::default()); @@ -190,7 +190,7 @@ fn encode_fails_for_length_too_large(#[case] fmt: LengthFormat, #[case] len: usi /// This test sends a `FailingResp` using `send_response` and asserts that the resulting /// error is of the `Serialize` variant, indicating a failure during response encoding. async fn send_response_returns_encode_error() { - let app = WireframeApp::<_, _, Envelope>::new().expect("failed to create app"); + let app = WireframeApp::::new().expect("failed to create app"); let err = app .send_response(&mut Vec::new(), &FailingResp) .await diff --git a/tests/routes.rs b/tests/routes.rs index e57a3b63..155702f4 100644 --- a/tests/routes.rs +++ b/tests/routes.rs @@ -56,7 +56,7 @@ struct Echo(u8); async fn handler_receives_message_and_echoes_response() { let called = Arc::new(AtomicUsize::new(0)); let called_clone = called.clone(); - let app = WireframeApp::<_, _, TestEnvelope>::new() + let app = WireframeApp::::new() .expect("failed to create app") .frame_processor(LengthPrefixedProcessor::default()) .route( @@ -97,7 +97,7 @@ async fn handler_receives_message_and_echoes_response() { #[tokio::test] async fn handler_echoes_with_none_correlation_id() { - let app = WireframeApp::<_, _, TestEnvelope>::new() + let app = WireframeApp::::new() .expect("failed to create app") .frame_processor(LengthPrefixedProcessor::default()) .route( @@ -130,7 +130,7 @@ async fn handler_echoes_with_none_correlation_id() { #[tokio::test] async fn multiple_frames_processed_in_sequence() { - let app = WireframeApp::<_, _, TestEnvelope>::new() + let app = WireframeApp::::new() .expect("failed to create app") .frame_processor(LengthPrefixedProcessor::default()) .route( @@ -191,7 +191,7 @@ async fn multiple_frames_processed_in_sequence() { #[case(Some(2))] #[tokio::test] async fn single_frame_propagates_correlation_id(#[case] cid: Option) { - let app = WireframeApp::<_, _, TestEnvelope>::new() + let app = WireframeApp::::new() .expect("failed to create app") .frame_processor(LengthPrefixedProcessor::default()) .route( diff --git a/tests/wireframe_protocol.rs b/tests/wireframe_protocol.rs index 774cb79e..9ee95480 100644 --- a/tests/wireframe_protocol.rs +++ b/tests/wireframe_protocol.rs @@ -19,6 +19,7 @@ use wireframe::{ app::{Envelope, WireframeApp}, connection::ConnectionActor, push::PushQueues, + serializer::BincodeSerializer, }; struct TestProtocol { @@ -51,7 +52,7 @@ async fn builder_produces_protocol_hooks() { let protocol = TestProtocol { counter: counter.clone(), }; - let app = WireframeApp::<_, _, Envelope>::new() + let app = WireframeApp::::new() .expect("failed to create app") .with_protocol(protocol); let mut hooks = app.protocol_hooks(); @@ -75,7 +76,7 @@ async fn connection_actor_uses_protocol_from_builder() { let protocol = TestProtocol { counter: counter.clone(), }; - let app = WireframeApp::<_, _, Envelope>::new() + let app = WireframeApp::::new() .expect("failed to create app") .with_protocol(protocol); diff --git a/tests/world.rs b/tests/world.rs index 9162a876..360f2d1e 100644 --- a/tests/world.rs +++ b/tests/world.rs @@ -15,6 +15,7 @@ use wireframe::{ hooks::ProtocolHooks, push::PushQueues, response::FrameStream, + serializer::BincodeSerializer, server::WireframeServer, }; @@ -35,7 +36,7 @@ struct PanicServer { impl PanicServer { async fn spawn() -> Self { let factory = || { - WireframeApp::new() + WireframeApp::::new() .expect("Failed to create WireframeApp") .on_connection_setup(|| async { panic!("boom") }) .expect("Failed to set connection setup callback") From f4c759eee1295bc8e9e619afba9eb735069d0582 Mon Sep 17 00:00:00 2001 From: Leynos Date: Wed, 27 Aug 2025 00:15:56 +0100 Subject: [PATCH 03/15] Clarify client builder framing docs --- README.md | 16 +++--- ...ge-fragmentation-and-re-assembly-design.md | 5 -- docs/rust-binary-router-library-design.md | 27 +++++----- docs/wireframe-client-design.md | 13 +++-- docs/wireframe-testing-crate.md | 2 - examples/echo.rs | 6 ++- examples/metadata_routing.rs | 7 +-- examples/packet_enum.rs | 8 +-- examples/ping_pong.rs | 8 +-- src/app/builder.rs | 49 +++++++------------ src/app/connection.rs | 11 +++-- tests/common/mod.rs | 12 ++--- tests/lifecycle.rs | 14 +++--- tests/metadata.rs | 11 +++-- tests/middleware_order.rs | 6 ++- tests/response.rs | 13 +++-- tests/routes.rs | 16 +++--- tests/wireframe_protocol.rs | 8 +-- tests/world.rs | 6 ++- wireframe_testing/src/helpers.rs | 21 ++++---- 20 files changed, 120 insertions(+), 139 deletions(-) diff --git a/README.md b/README.md index a9820cc1..92dcc5c6 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ connections and runs the Tokio event loop: ```rust WireframeServer::new(|| { WireframeApp::new() - .frame_processor(MyFrameProcessor::new()) .app_data(state.clone()) .route(MessageType::Login, handle_login) .wrap(MyLoggingMiddleware::default()) @@ -48,10 +47,10 @@ WireframeServer::new(|| { 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. `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` +The builder supports methods like `route`, `app_data`, and `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` extractor【F:docs/rust-binary-router-library-design.md†L622-L710】. Handlers are asynchronous functions whose parameters implement extractor traits @@ -62,7 +61,7 @@ concise【F:docs/rust-binary-router-library-design.md†L682-L710】. ## Example The design document includes a simple echo server that demonstrates routing -based on a message ID and the use of a length‑prefixed frame processor: +based on a message ID and the use of a length‑delimited codec: ```rust async fn handle_echo(req: Message) -> WireframeResult { @@ -139,10 +138,7 @@ size and endianness) and defaults to a 4‑byte big‑endian length prefix【F:docs/rust-binary-router-library-design.md†L1082-L1123】. ```rust -use wireframe::frame::{LengthFormat, LengthPrefixedProcessor}; - -let app = WireframeApp::new()? - .frame_processor(LengthPrefixedProcessor::new(LengthFormat::u16_le())); +let app = WireframeApp::new()?; ``` ## Connection Lifecycle diff --git a/docs/generic-message-fragmentation-and-re-assembly-design.md b/docs/generic-message-fragmentation-and-re-assembly-design.md index 027bf0e8..f173b120 100644 --- a/docs/generic-message-fragmentation-and-re-assembly-design.md +++ b/docs/generic-message-fragmentation-and-re-assembly-design.md @@ -162,11 +162,6 @@ Developers will enable fragmentation by adding the `FragmentAdapter` to their // Example: Configuring a server for MySQL-style fragmentation. WireframeServer::new(|| { WireframeApp::new() - .frame_processor( - FragmentAdapter::new(MySqlStrategy) - .with_max_message_size(64 * 1024 * 1024) // 64 MiB - .with_reassembly_timeout(Duration::from_secs(30)) - ) .route(...) }) ``` diff --git a/docs/rust-binary-router-library-design.md b/docs/rust-binary-router-library-design.md index 15df8665..34debb68 100644 --- a/docs/rust-binary-router-library-design.md +++ b/docs/rust-binary-router-library-design.md @@ -726,8 +726,7 @@ component to run it. async fn main_server_setup() -> std::io::Result<()> { let app_state = Arc::new(Mutex::new(AppState::new())); WireframeServer::new(move || { // Closure provides App per worker thread - WireframeApp::new() - .frame_processor(MyFrameProcessor::new()) // Configure the framing logic + WireframeApp::new() .app_data(app_state.clone()) // Shared application state //.service(login_handler) // If using attribute macros and auto-discovery //.service(chat_handler) @@ -745,10 +744,8 @@ The WireframeApp builder would offer methods like: - WireframeApp::new(): Creates a new application builder. -- .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. +- `[deprecated]` `.frame_processor(impl FrameProcessor)`: framing is now + handled by the connection codec. - .route(message_id, handler_function): Explicitly maps a message identifier to a handler. @@ -1273,7 +1270,7 @@ its own `FrameProcessor` trait or provide helpers.) WireframeServer::new(|| { WireframeApp::new() - //.frame_processor(LengthPrefixedCodec) // Simplified + //.frame_processor(LengthPrefixedCodec) // deprecated: framing handled by codec .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: @@ -1383,14 +1380,14 @@ simplify server implementation. let chat_state = Arc::new(Mutex::new(ChatRoomState { users: HashMap::new() })); - WireframeServer::new(move || { - WireframeApp::new() - //.frame_processor(...) - .serializer(BincodeSerializer) - .app_data(chat_state.clone()) - .route(ChatMessageType::ClientJoin, handle_join) - .route(ChatMessageType::ClientPost, handle_post) - }) + WireframeServer::new(move || { + WireframeApp::new() + //.frame_processor(...) // deprecated: framing handled by codec + .serializer(BincodeSerializer) + .app_data(chat_state.clone()) + .route(ChatMessageType::ClientJoin, handle_join) + .route(ChatMessageType::ClientPost, handle_post) + }) .bind("127.0.0.1:8001")? .run() .await diff --git a/docs/wireframe-client-design.md b/docs/wireframe-client-design.md index abca896c..731cde8d 100644 --- a/docs/wireframe-client-design.md +++ b/docs/wireframe-client-design.md @@ -33,14 +33,14 @@ A `WireframeClient::builder()` method configures the client: ```rust let client = WireframeClient::builder() - .frame_processor(LengthPrefixedProcessor::new(LengthFormat::u32_be())) .serializer(BincodeSerializer) .connect("127.0.0.1:7878") .await?; ``` -The same `FrameProcessor` and `Serializer` traits used by the server are reused -here, ensuring messages are framed and encoded consistently. +The same `Serializer` trait used by the server is reused here, ensuring +messages are encoded consistently while framing is handled by the +length‑delimited codec. ### Request/Response Helpers @@ -52,9 +52,9 @@ let request = Login { username: "guest".into() }; let response: LoginAck = client.call(request).await?; ``` -Internally, this uses the `Serializer` to encode the request, writes it through -the `FrameProcessor`, then waits for a frame, decodes it, and deserializes the -response type. +Internally, this uses the `Serializer` to encode the request, sends it through +the length‑delimited codec, then waits for a frame, decodes it, and +deserializes the response type. ### Connection Lifecycle @@ -68,7 +68,6 @@ initialization logic. #[tokio::main] async fn main() -> std::io::Result<()> { let mut client = WireframeClient::builder() - .frame_processor(LengthPrefixedProcessor::new(LengthFormat::u32_be())) .serializer(BincodeSerializer) .connect("127.0.0.1:7878") .await?; diff --git a/docs/wireframe-testing-crate.md b/docs/wireframe-testing-crate.md index 3972a71f..6152384a 100644 --- a/docs/wireframe-testing-crate.md +++ b/docs/wireframe-testing-crate.md @@ -135,14 +135,12 @@ let (_, frame) = recv_expect!(queues.recv()); ```rust use std::sync::Arc; use wireframe_testing::{drive_with_frame, drive_with_frames}; -use wireframe::processor::LengthPrefixedProcessor; use crate::tests::{build_test_frame, expected_bytes}; #[tokio::test] async fn handler_echoes_message() { let app = WireframeApp::new() .unwrap() - .frame_processor(LengthPrefixedProcessor::default()) .route(1, Arc::new(|_| Box::pin(async {}))) .unwrap(); diff --git a/examples/echo.rs b/examples/echo.rs index 43c2e795..91ae5f1d 100644 --- a/examples/echo.rs +++ b/examples/echo.rs @@ -4,15 +4,17 @@ //! envelope back to the client. use wireframe::{ - app::{Envelope, WireframeApp}, + app::Envelope, serializer::BincodeSerializer, server::{ServerError, WireframeServer}, }; +type App = wireframe::app::WireframeApp; + #[tokio::main] async fn main() -> Result<(), ServerError> { let factory = || { - WireframeApp::::new() + App::new() .expect("failed to create WireframeApp") .route( 1, diff --git a/examples/metadata_routing.rs b/examples/metadata_routing.rs index 9d9bedaf..68c7fc24 100644 --- a/examples/metadata_routing.rs +++ b/examples/metadata_routing.rs @@ -8,12 +8,14 @@ use std::{io, sync::Arc}; use bytes::BytesMut; use tokio::io::{AsyncWriteExt, duplex}; use wireframe::{ - app::{Envelope, WireframeApp}, + app::Envelope, frame::{FrameMetadata, FrameProcessor, LengthPrefixedProcessor}, message::Message, serializer::{BincodeSerializer, Serializer}, }; +type App = wireframe::app::WireframeApp; + /// Frame format with a two-byte id, one-byte flags, and bincode payload. struct HeaderSerializer; @@ -61,9 +63,8 @@ struct Ping; #[tokio::main] async fn main() -> io::Result<()> { - let app = WireframeApp::::new() + let app = App::new() .expect("failed to create app") - .frame_processor(LengthPrefixedProcessor::default()) .serializer(HeaderSerializer) .route( 1, diff --git a/examples/packet_enum.rs b/examples/packet_enum.rs index 5c85fbb0..92f2f808 100644 --- a/examples/packet_enum.rs +++ b/examples/packet_enum.rs @@ -7,14 +7,15 @@ use std::{collections::HashMap, future::Future, pin::Pin}; use async_trait::async_trait; use wireframe::{ - app::{Envelope, WireframeApp}, - frame::{LengthFormat, LengthPrefixedProcessor}, + app::Envelope, message::Message, middleware::{HandlerService, Service, ServiceRequest, ServiceResponse, Transform}, serializer::BincodeSerializer, server::{ServerError, WireframeServer}, }; +type App = wireframe::app::WireframeApp; + #[derive(bincode::Encode, bincode::BorrowDecode, Debug)] enum Packet { Ping, @@ -79,9 +80,8 @@ fn handle_packet(_env: &Envelope) -> Pin + Send>> { #[tokio::main] async fn main() -> Result<(), ServerError> { let factory = || { - WireframeApp::::new() + App::new() .expect("Failed to create WireframeApp") - .frame_processor(LengthPrefixedProcessor::new(LengthFormat::u16_le())) .wrap(DecodeMiddleware) .expect("Failed to wrap middleware") .route(1, std::sync::Arc::new(handle_packet)) diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs index f5681043..9d9810ad 100644 --- a/examples/ping_pong.rs +++ b/examples/ping_pong.rs @@ -7,13 +7,15 @@ use std::{net::SocketAddr, sync::Arc}; use async_trait::async_trait; use wireframe::{ - app::{Envelope, Packet, Result as AppResult, WireframeApp}, + app::{Envelope, Packet, Result as AppResult}, message::Message, middleware::{HandlerService, Service, ServiceRequest, ServiceResponse, Transform}, serializer::BincodeSerializer, server::{ServerError, WireframeServer}, }; +type App = wireframe::app::WireframeApp; + #[derive(bincode::Encode, bincode::BorrowDecode, Debug)] struct Ping(u32); @@ -130,8 +132,8 @@ impl Transform> for Logging { } } -fn build_app() -> AppResult> { - WireframeApp::::new()? +fn build_app() -> AppResult { + App::new()? .serializer(BincodeSerializer) .route(PING_ID, Arc::new(|_: &Envelope| Box::pin(ping_handler())))? .wrap(PongMiddleware)? diff --git a/src/app/builder.rs b/src/app/builder.rs index 8d7e8c68..ef14575a 100644 --- a/src/app/builder.rs +++ b/src/app/builder.rs @@ -1,7 +1,8 @@ //! Application builder configuring routes and middleware. -//! This module defines [`WireframeApp`], an Actix-inspired builder for -//! managing connection state, routing, and middleware in a `WireframeServer`. -//! It exposes convenience methods to register handlers and lifecycle hooks. +//! [`WireframeApp`] is an Actix-inspired builder for managing connection +//! state, routing, and middleware in a `WireframeServer`. It exposes +//! convenience methods to register handlers and lifecycle hooks, and +//! serializes messages using a configurable serializer. use std::{ any::{Any, TypeId}, @@ -22,13 +23,10 @@ use super::{ error::{Result, WireframeError}, }; use crate::{ - frame::{FrameProcessor, LengthFormat, LengthPrefixedProcessor}, hooks::{ProtocolHooks, WireframeProtocol}, middleware::{HandlerService, Transform}, serializer::{BincodeSerializer, Serializer}, }; -type BoxedFrameProcessor = - Box, Error = io::Error> + Send + Sync>; /// Callback invoked when a connection is established. /// @@ -68,9 +66,9 @@ pub type ConnectionTeardown = /// Configures routing and middleware for a `WireframeServer`. /// -/// The builder stores registered routes, services, and middleware -/// without enforcing an ordering. Methods return [`Result`] so -/// registrations can be chained ergonomically. +/// The builder stores registered routes and middleware without enforcing an +/// ordering. Methods return [`Result`] so registrations can be chained +/// ergonomically. pub struct WireframeApp< S: Serializer + Send + Sync = BincodeSerializer, C: Send + 'static = (), @@ -78,9 +76,10 @@ pub struct WireframeApp< > { pub(super) handlers: HashMap>, pub(super) routes: OnceCell>>>, - pub(super) services: Vec>, pub(super) middleware: Vec, Output = HandlerService>>>, - pub(super) frame_processor: BoxedFrameProcessor, + #[allow(dead_code)] + pub(super) frame_processor: + Box, Error = io::Error> + Send + Sync>, pub(super) serializer: S, pub(super) app_data: HashMap>, pub(super) on_connect: Option>>, @@ -115,16 +114,17 @@ where E: Packet, { /// - /// Initializes empty routes, services, middleware, and application data. - /// Sets the default frame processor and serializer, with no connection - /// lifecycle hooks. + /// Initializes empty routes, middleware, and application data. Sets a + /// placeholder frame processor and serializer, with no connection lifecycle + /// hooks. fn default() -> Self { Self { handlers: HashMap::new(), routes: OnceCell::new(), - services: Vec::new(), middleware: Vec::new(), - frame_processor: Box::new(LengthPrefixedProcessor::new(LengthFormat::default())), + frame_processor: Box::new(crate::frame::LengthPrefixedProcessor::new( + crate::frame::LengthFormat::default(), + )), serializer: S::default(), app_data: HashMap::new(), on_connect: None, @@ -193,18 +193,6 @@ where Ok(self) } - /// Register a handler discovered by attribute macros or other means. - /// - /// # Errors - /// - /// This function always succeeds currently but uses [`Result`] for - /// consistency with other builder methods. - pub fn service(mut self, handler: Handler) -> Result { - self.services.push(handler); - self.routes = OnceCell::new(); - Ok(self) - } - /// Store a shared state value accessible to request extractors. /// /// The value can later be retrieved using [`crate::extractor::SharedState`]. Registering @@ -260,7 +248,6 @@ where Ok(WireframeApp { handlers: self.handlers, routes: OnceCell::new(), - services: self.services, middleware: self.middleware, frame_processor: self.frame_processor, serializer: self.serializer, @@ -351,10 +338,11 @@ where } /// Set the frame processor used for encoding and decoding frames. + #[deprecated(note = "framing is handled by the connection codec; this method will be removed")] #[must_use] pub fn frame_processor

(self, processor: P) -> Self where - P: FrameProcessor, Error = io::Error> + Send + Sync + 'static, + P: crate::frame::FrameProcessor, Error = io::Error> + Send + Sync + 'static, { WireframeApp { frame_processor: Box::new(processor), @@ -371,7 +359,6 @@ where WireframeApp { handlers: self.handlers, routes: OnceCell::new(), - services: self.services, middleware: self.middleware, frame_processor: self.frame_processor, serializer, diff --git a/src/app/connection.rs b/src/app/connection.rs index 7e7e060d..fab667e4 100644 --- a/src/app/connection.rs +++ b/src/app/connection.rs @@ -8,7 +8,7 @@ use tokio::{ io::{self, AsyncRead, AsyncWrite, AsyncWriteExt}, time::{Duration, timeout}, }; -use tokio_util::codec::{Framed, LengthDelimitedCodec}; +use tokio_util::codec::{Encoder, Framed, LengthDelimitedCodec}; use super::{ builder::WireframeApp, @@ -31,7 +31,7 @@ where C: Send + 'static, E: Packet, { - /// Serialize `msg` and write it to `stream` using the frame processor. + /// Serialize `msg` and write it to `stream` using a length-delimited codec. /// /// # Errors /// @@ -49,9 +49,10 @@ where .serializer .serialize(msg) .map_err(SendError::Serialize)?; - let mut framed = BytesMut::with_capacity(4 + bytes.len()); - self.frame_processor - .encode(&bytes, &mut framed) + let mut codec = LengthDelimitedCodec::new(); + let mut framed = BytesMut::new(); + codec + .encode(bytes.into(), &mut framed) .map_err(SendError::Io)?; stream.write_all(&framed).await.map_err(SendError::Io)?; stream.flush().await.map_err(SendError::Io) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d7e36482..0d177f8c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -13,17 +13,15 @@ pub fn unused_listener() -> StdTcpListener { } use rstest::fixture; -use wireframe::{ - app::{Envelope, WireframeApp}, - serializer::BincodeSerializer, -}; +use wireframe::{app::Envelope, serializer::BincodeSerializer}; + +type TestApp = wireframe::app::WireframeApp; #[fixture] #[allow( unused_braces, reason = "rustc false positive for single line rstest fixtures" )] -pub fn factory() --> impl Fn() -> WireframeApp + Send + Sync + Clone + 'static { - || WireframeApp::::new().expect("WireframeApp::new failed") +pub fn factory() -> impl Fn() -> TestApp + Send + Sync + Clone + 'static { + || TestApp::new().expect("WireframeApp::new failed") } diff --git a/tests/lifecycle.rs b/tests/lifecycle.rs index ae0fecc0..ba2810a3 100644 --- a/tests/lifecycle.rs +++ b/tests/lifecycle.rs @@ -13,12 +13,15 @@ use std::{ use bytes::BytesMut; use wireframe::{ - app::{Envelope, Packet, PacketParts, WireframeApp}, + app::{Envelope, Packet, PacketParts}, frame::FrameProcessor, serializer::{BincodeSerializer, Serializer}, }; use wireframe_testing::{processor, run_app, run_with_duplex_server}; +type App = wireframe::app::WireframeApp; +type BasicApp = wireframe::app::WireframeApp; + fn call_counting_callback( counter: &Arc, result: R, @@ -42,14 +45,14 @@ fn wireframe_app_with_lifecycle_callbacks( setup: &Arc, teardown: &Arc, state: u32, -) -> WireframeApp +) -> App where E: Packet, { let setup_cb = call_counting_callback(setup, state); let teardown_cb = call_counting_callback(teardown, ()); - WireframeApp::::new() + App::::new() .expect("failed to create app") .on_connection_setup(move || setup_cb(())) .expect("setup callback") @@ -74,7 +77,7 @@ async fn setup_without_teardown_runs() { let setup_count = Arc::new(AtomicUsize::new(0)); let cb = call_counting_callback(&setup_count, ()); - let app = WireframeApp::::new() + let app = BasicApp::new() .expect("failed to create app") .on_connection_setup(move || cb(())) .expect("setup callback"); @@ -89,7 +92,7 @@ async fn teardown_without_setup_does_not_run() { let teardown_count = Arc::new(AtomicUsize::new(0)); let cb = call_counting_callback(&teardown_count, ()); - let app = WireframeApp::::new() + let app = BasicApp::new() .expect("failed to create app") .on_connection_teardown(cb) .expect("teardown callback"); @@ -133,7 +136,6 @@ async fn helpers_propagate_connection_state() { let teardown = Arc::new(AtomicUsize::new(0)); let app = wireframe_app_with_lifecycle_callbacks::(&setup, &teardown, 7) - .frame_processor(processor()) .route(1, Arc::new(|_: &StateEnvelope| Box::pin(async {}))) .expect("route registration failed"); diff --git a/tests/metadata.rs b/tests/metadata.rs index b581977e..f1af3de2 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -8,19 +8,20 @@ use std::sync::{ }; use wireframe::{ - app::{Envelope, WireframeApp}, - frame::{FrameMetadata, LengthPrefixedProcessor}, + app::Envelope, + frame::FrameMetadata, serializer::{BincodeSerializer, Serializer}, }; use wireframe_testing::{TestSerializer, drive_with_bincode}; -fn mock_wireframe_app_with_serializer(serializer: S) -> WireframeApp +type TestApp = wireframe::app::WireframeApp; + +fn mock_wireframe_app_with_serializer(serializer: S) -> TestApp where S: TestSerializer, { - WireframeApp::::new() + TestApp::::new() .expect("failed to create app") - .frame_processor(LengthPrefixedProcessor::default()) .serializer(serializer) .route(1, Arc::new(|_| Box::pin(async {}))) .expect("route registration failed") diff --git a/tests/middleware_order.rs b/tests/middleware_order.rs index 1e1df434..f823d467 100644 --- a/tests/middleware_order.rs +++ b/tests/middleware_order.rs @@ -6,12 +6,14 @@ use async_trait::async_trait; use bytes::BytesMut; use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex}; use wireframe::{ - app::{Envelope, Handler, WireframeApp}, + app::{Envelope, Handler}, frame::{FrameProcessor, LengthPrefixedProcessor}, middleware::{HandlerService, Service, ServiceRequest, ServiceResponse, Transform}, serializer::{BincodeSerializer, Serializer}, }; +type TestApp = wireframe::app::WireframeApp; + struct TagMiddleware(u8); struct TagService { @@ -53,7 +55,7 @@ impl Transform> for TagMiddleware { #[tokio::test] async fn middleware_applied_in_reverse_order() { let handler: Handler = std::sync::Arc::new(|_env: &Envelope| Box::pin(async {})); - let app = WireframeApp::::new() + let app = TestApp::new() .expect("failed to create app") .route(1, handler) .expect("route registration failed") diff --git a/tests/response.rs b/tests/response.rs index e13bcc84..8596b899 100644 --- a/tests/response.rs +++ b/tests/response.rs @@ -6,12 +6,14 @@ use bytes::BytesMut; use rstest::rstest; use wireframe::{ - app::{Envelope, WireframeApp}, + app::Envelope, frame::{Endianness, FrameProcessor, LengthFormat, LengthPrefixedProcessor}, message::Message, serializer::BincodeSerializer, }; +type TestApp = wireframe::app::WireframeApp; + #[derive(bincode::Encode, bincode::BorrowDecode, PartialEq, Debug)] struct TestResp(u32); @@ -39,9 +41,8 @@ impl<'de> bincode::BorrowDecode<'de, ()> for FailingResp { /// Tests that sending a response serialises and frames the data correctly, /// and that the response can be decoded and deserialised back to its original value asynchronously. async fn send_response_encodes_and_frames() { - let app = WireframeApp::::new() + let app = TestApp::new() .expect("failed to create app") - .frame_processor(LengthPrefixedProcessor::default()) .serializer(BincodeSerializer); let mut out = Vec::new(); @@ -131,9 +132,7 @@ fn custom_length_roundtrip( #[tokio::test] async fn send_response_propagates_write_error() { - let app = WireframeApp::::new() - .expect("app creation failed") - .frame_processor(LengthPrefixedProcessor::default()); + let app = TestApp::new().expect("app creation failed"); let mut writer = FailingWriter; let err = app @@ -190,7 +189,7 @@ fn encode_fails_for_length_too_large(#[case] fmt: LengthFormat, #[case] len: usi /// This test sends a `FailingResp` using `send_response` and asserts that the resulting /// error is of the `Serialize` variant, indicating a failure during response encoding. async fn send_response_returns_encode_error() { - let app = WireframeApp::::new().expect("failed to create app"); + let app = TestApp::new().expect("failed to create app"); let err = app .send_response(&mut Vec::new(), &FailingResp) .await diff --git a/tests/routes.rs b/tests/routes.rs index 155702f4..aafc793b 100644 --- a/tests/routes.rs +++ b/tests/routes.rs @@ -11,13 +11,15 @@ use bytes::BytesMut; use rstest::rstest; use wireframe::{ Serializer, - app::{Packet, PacketParts, WireframeApp}, + app::{Packet, PacketParts}, frame::{FrameProcessor, LengthPrefixedProcessor}, message::Message, serializer::BincodeSerializer, }; use wireframe_testing::{drive_with_bincode, drive_with_frames}; +type TestApp = wireframe::app::WireframeApp; + #[derive(bincode::Encode, bincode::BorrowDecode, PartialEq, Debug, Clone)] struct TestEnvelope { id: u32, @@ -56,9 +58,8 @@ struct Echo(u8); async fn handler_receives_message_and_echoes_response() { let called = Arc::new(AtomicUsize::new(0)); let called_clone = called.clone(); - let app = WireframeApp::::new() + let app = TestApp::new() .expect("failed to create app") - .frame_processor(LengthPrefixedProcessor::default()) .route( 1, std::sync::Arc::new(move |_: &TestEnvelope| { @@ -97,9 +98,8 @@ async fn handler_receives_message_and_echoes_response() { #[tokio::test] async fn handler_echoes_with_none_correlation_id() { - let app = WireframeApp::::new() + let app = TestApp::new() .expect("failed to create app") - .frame_processor(LengthPrefixedProcessor::default()) .route( 1, std::sync::Arc::new(|_: &TestEnvelope| Box::pin(async {})), @@ -130,9 +130,8 @@ async fn handler_echoes_with_none_correlation_id() { #[tokio::test] async fn multiple_frames_processed_in_sequence() { - let app = WireframeApp::::new() + let app = TestApp::new() .expect("failed to create app") - .frame_processor(LengthPrefixedProcessor::default()) .route( 1, std::sync::Arc::new(|_: &TestEnvelope| Box::pin(async {})), @@ -191,9 +190,8 @@ async fn multiple_frames_processed_in_sequence() { #[case(Some(2))] #[tokio::test] async fn single_frame_propagates_correlation_id(#[case] cid: Option) { - let app = WireframeApp::::new() + let app = TestApp::new() .expect("failed to create app") - .frame_processor(LengthPrefixedProcessor::default()) .route( 1, std::sync::Arc::new(|_: &TestEnvelope| Box::pin(async {})), diff --git a/tests/wireframe_protocol.rs b/tests/wireframe_protocol.rs index 9ee95480..72fbaa85 100644 --- a/tests/wireframe_protocol.rs +++ b/tests/wireframe_protocol.rs @@ -16,12 +16,14 @@ use tokio_util::sync::CancellationToken; use wireframe::{ ConnectionContext, WireframeProtocol, - app::{Envelope, WireframeApp}, + app::Envelope, connection::ConnectionActor, push::PushQueues, serializer::BincodeSerializer, }; +type TestApp = wireframe::app::WireframeApp; + struct TestProtocol { counter: Arc, } @@ -52,7 +54,7 @@ async fn builder_produces_protocol_hooks() { let protocol = TestProtocol { counter: counter.clone(), }; - let app = WireframeApp::::new() + let app = TestApp::new() .expect("failed to create app") .with_protocol(protocol); let mut hooks = app.protocol_hooks(); @@ -76,7 +78,7 @@ async fn connection_actor_uses_protocol_from_builder() { let protocol = TestProtocol { counter: counter.clone(), }; - let app = WireframeApp::::new() + let app = TestApp::new() .expect("failed to create app") .with_protocol(protocol); diff --git a/tests/world.rs b/tests/world.rs index 360f2d1e..303e5f31 100644 --- a/tests/world.rs +++ b/tests/world.rs @@ -10,7 +10,7 @@ use cucumber::World; use tokio::{net::TcpStream, sync::oneshot}; use tokio_util::sync::CancellationToken; use wireframe::{ - app::{Envelope, Packet, WireframeApp}, + app::{Envelope, Packet}, connection::ConnectionActor, hooks::ProtocolHooks, push::PushQueues, @@ -19,6 +19,8 @@ use wireframe::{ server::WireframeServer, }; +type TestApp = wireframe::app::WireframeApp; + #[path = "common/mod.rs"] mod common; use common::unused_listener; @@ -36,7 +38,7 @@ struct PanicServer { impl PanicServer { async fn spawn() -> Self { let factory = || { - WireframeApp::::new() + TestApp::new() .expect("Failed to create WireframeApp") .on_connection_setup(|| async { panic!("boom") }) .expect("Failed to set connection setup callback") diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index 27f66fdc..b3ae67ca 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -161,7 +161,7 @@ macro_rules! forward_with_capacity { /// # use wireframe_testing::{drive_with_frame, processor}; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().frame_processor(processor()).unwrap(); +/// let app = WireframeApp::new().unwrap(); /// let bytes = drive_with_frame(app, vec![1, 2, 3]).await?; /// # Ok(()) /// # } @@ -187,7 +187,7 @@ forward_with_capacity! { /// # use wireframe_testing::{drive_with_frame_with_capacity, processor}; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> tokio::io::Result<()> { - /// let app = WireframeApp::new().frame_processor(processor()).unwrap(); + /// let app = WireframeApp::new().unwrap(); /// let bytes = drive_with_frame_with_capacity(app, vec![0], 512).await?; /// # Ok(()) /// # } @@ -205,7 +205,7 @@ forward_default! { /// # use wireframe_testing::{drive_with_frames, processor}; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> tokio::io::Result<()> { - /// let app = WireframeApp::new().frame_processor(processor()).unwrap(); + /// let app = WireframeApp::new().unwrap(); /// let out = drive_with_frames(app, vec![vec![1], vec![2]]).await?; /// # Ok(()) /// # } @@ -222,7 +222,7 @@ forward_default! { /// # use wireframe_testing::{drive_with_frames_with_capacity, processor}; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().frame_processor(processor()).unwrap(); +/// let app = WireframeApp::new().unwrap(); /// let out = drive_with_frames_with_capacity(app, vec![vec![1], vec![2]], 1024).await?; /// # Ok(()) /// # } @@ -253,7 +253,7 @@ forward_default! { /// # use wireframe_testing::{drive_with_frame_mut, processor}; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> tokio::io::Result<()> { - /// let mut app = WireframeApp::new().frame_processor(processor()).unwrap(); + /// let mut app = WireframeApp::new().unwrap(); /// let bytes = drive_with_frame_mut(&mut app, vec![1]).await?; /// # Ok(()) /// # } @@ -269,7 +269,7 @@ forward_with_capacity! { /// # use wireframe_testing::{drive_with_frame_with_capacity_mut, processor}; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> tokio::io::Result<()> { - /// let mut app = WireframeApp::new().frame_processor(processor()).unwrap(); + /// let mut app = WireframeApp::new().unwrap(); /// let bytes = drive_with_frame_with_capacity_mut(&mut app, vec![1], 256).await?; /// # Ok(()) /// # } @@ -285,7 +285,7 @@ forward_default! { /// # use wireframe_testing::{drive_with_frames_mut, processor}; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> tokio::io::Result<()> { - /// let mut app = WireframeApp::new().frame_processor(processor()).unwrap(); + /// let mut app = WireframeApp::new().unwrap(); /// let out = drive_with_frames_mut(&mut app, vec![vec![1], vec![2]]).await?; /// # Ok(()) /// # } @@ -300,7 +300,7 @@ forward_default! { /// # use wireframe_testing::{drive_with_frames_with_capacity_mut, processor}; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> tokio::io::Result<()> { -/// let mut app = WireframeApp::new().frame_processor(processor()).unwrap(); +/// let mut app = WireframeApp::new().unwrap(); /// let out = drive_with_frames_with_capacity_mut(&mut app, vec![vec![1], vec![2]], 64).await?; /// # Ok(()) /// # } @@ -331,7 +331,7 @@ where /// #[derive(bincode::Encode)] /// struct Ping(u8); /// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().frame_processor(processor()).unwrap(); +/// let app = WireframeApp::new().unwrap(); /// let bytes = drive_with_bincode(app, Ping(1)).await?; /// # Ok(()) /// # } @@ -373,7 +373,7 @@ where /// # use wireframe_testing::{processor, run_app}; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().frame_processor(processor()).unwrap(); +/// let app = WireframeApp::new().unwrap(); /// let out = run_app(app, vec![vec![1]], None).await?; /// # Ok(()) /// # } @@ -437,7 +437,6 @@ where /// # use wireframe::app::WireframeApp; /// # async fn demo() { /// let app = WireframeApp::new() -/// .frame_processor(processor()) /// .unwrap(); /// run_with_duplex_server(app).await; /// } From 7a9c84015c7e640f522a80590625bd9a58cf4a2d Mon Sep 17 00:00:00 2001 From: Leynos Date: Wed, 27 Aug 2025 08:16:35 +0100 Subject: [PATCH 04/15] Clamp connection buffers and unify framing --- src/app/builder.rs | 60 ++++++++++++++++++++++++++----------------- src/app/connection.rs | 34 +++++++++++++++++++----- src/app/envelope.rs | 41 ++++++++++++++++------------- src/app/error.rs | 41 ++++++++++------------------- src/app/mod.rs | 11 +++++++- tests/metadata.rs | 3 +-- 6 files changed, 111 insertions(+), 79 deletions(-) diff --git a/src/app/builder.rs b/src/app/builder.rs index ef14575a..c7568c3e 100644 --- a/src/app/builder.rs +++ b/src/app/builder.rs @@ -27,7 +27,6 @@ use crate::{ middleware::{HandlerService, Transform}, serializer::{BincodeSerializer, Serializer}, }; - /// Callback invoked when a connection is established. /// /// # Examples @@ -36,13 +35,8 @@ use crate::{ /// use std::sync::Arc; /// /// use wireframe::app::ConnectionSetup; -/// -/// let setup: Arc> = Arc::new(|| { -/// Box::pin(async { -/// // Perform authentication and return connection state -/// String::from("hello") -/// }) -/// }); +/// let setup: Arc> = +/// Arc::new(|| Box::pin(async { String::from("hello") })); /// ``` pub type ConnectionSetup = dyn Fn() -> Pin + Send>> + Send + Sync; @@ -54,7 +48,6 @@ pub type ConnectionSetup = dyn Fn() -> Pin + Send> /// use std::sync::Arc; /// /// use wireframe::app::ConnectionTeardown; -/// /// let teardown: Arc> = Arc::new(|state| { /// Box::pin(async move { /// println!("Dropping {state}"); @@ -63,7 +56,6 @@ pub type ConnectionSetup = dyn Fn() -> Pin + Send> /// ``` pub type ConnectionTeardown = dyn Fn(C) -> Pin + Send>> + Send + Sync; - /// Configures routing and middleware for a `WireframeServer`. /// /// The builder stores registered routes and middleware without enforcing an @@ -92,8 +84,7 @@ pub struct WireframeApp< /// Alias for asynchronous route handlers. /// -/// A `Handler` is an `Arc` to a function returning a [`Future`], enabling -/// asynchronous execution of message handlers. +/// A `Handler` wraps an `Arc` to a function returning a [`Future`]. pub type Handler = Arc Pin + Send>> + Send + Sync>; /// Trait representing middleware components. @@ -113,10 +104,8 @@ where C: Send + 'static, E: Packet, { - /// - /// Initializes empty routes, middleware, and application data. Sets a - /// placeholder frame processor and serializer, with no connection lifecycle - /// hooks. + /// Initializes empty routes, middleware, and application data with a + /// placeholder frame processor and serializer, and no lifecycle hooks. fn default() -> Self { Self { handlers: HashMap::new(), @@ -154,8 +143,7 @@ where /// /// ``` /// use wireframe::app::WireframeApp; - /// let app = WireframeApp::<_, _, wireframe::app::Envelope>::new().unwrap(); - /// assert!(app.protocol().is_none()); + /// WireframeApp::<_, _, wireframe::app::Envelope>::new().unwrap(); /// ``` pub fn new() -> Result { Ok(Self::default()) } @@ -166,8 +154,7 @@ where /// /// # Errors /// - /// This function currently never returns an error but uses [`Result`] for - /// forward compatibility. + /// Currently always succeeds. #[deprecated(note = "use `WireframeApp::new()` instead")] pub fn new_with_envelope() -> Result { Self::new() } } @@ -178,6 +165,31 @@ where C: Send + 'static, E: Packet, { + /// Construct a new application builder using the provided serializer. + /// + /// # Errors + /// + /// This function currently never returns an error but uses [`Result`] for + /// forward compatibility. + pub fn with_serializer(serializer: S) -> Result { + Ok(Self { + handlers: HashMap::new(), + routes: OnceCell::new(), + middleware: Vec::new(), + frame_processor: Box::new(crate::frame::LengthPrefixedProcessor::new( + crate::frame::LengthFormat::default(), + )), + serializer, + app_data: HashMap::new(), + on_connect: None, + on_disconnect: None, + protocol: None, + push_dlq: None, + buffer_capacity: 1024, + read_timeout_ms: 100, + }) + } + /// Register a route that maps `id` to `handler`. /// /// # Errors @@ -372,16 +384,16 @@ where } } - /// Set the initial buffer capacity for framed reads. + /// Set the initial buffer capacity for framed reads (clamped to ≥64). #[must_use] pub fn buffer_capacity(mut self, capacity: usize) -> Self { - self.buffer_capacity = capacity; + self.buffer_capacity = capacity.max(64); self } - /// Configure the read timeout in milliseconds. + /// Configure the read timeout in milliseconds (clamped to ≥1). #[must_use] pub fn read_timeout_ms(mut self, timeout_ms: u64) -> Self { - self.read_timeout_ms = timeout_ms; + self.read_timeout_ms = timeout_ms.max(1); self } } diff --git a/src/app/connection.rs b/src/app/connection.rs index fab667e4..18d11d99 100644 --- a/src/app/connection.rs +++ b/src/app/connection.rs @@ -25,6 +25,12 @@ use crate::{ /// Maximum consecutive deserialization failures before closing a connection. const MAX_DESER_FAILURES: u32 = 10; +#[derive(Debug)] +enum EnvelopeDecodeError { + Parse(E), + Deserialize(Box), +} + impl WireframeApp where S: Serializer + Send + Sync, @@ -49,11 +55,11 @@ where .serializer .serialize(msg) .map_err(SendError::Serialize)?; - let mut codec = LengthDelimitedCodec::new(); + let mut codec = LengthDelimitedCodec::builder().new_codec(); let mut framed = BytesMut::new(); codec .encode(bytes.into(), &mut framed) - .map_err(SendError::Io)?; + .map_err(|e| SendError::Io(io::Error::new(io::ErrorKind::InvalidData, e)))?; stream.write_all(&framed).await.map_err(SendError::Io)?; stream.flush().await.map_err(SendError::Io) } @@ -70,11 +76,15 @@ where fn parse_envelope( &self, frame: &[u8], - ) -> std::result::Result<(Envelope, usize), Box> { + ) -> std::result::Result<(Envelope, usize), EnvelopeDecodeError> { self.serializer .parse(frame) - .map_err(|e| Box::new(e) as Box) - .or_else(|_| self.serializer.deserialize::(frame)) + .map_err(EnvelopeDecodeError::Parse) + .or_else(|_| { + self.serializer + .deserialize::(frame) + .map_err(EnvelopeDecodeError::Deserialize) + }) } /// Handle an accepted connection. @@ -158,7 +168,19 @@ where *deser_failures = 0; result } - Err(e) => { + Err(EnvelopeDecodeError::Parse(e)) => { + *deser_failures += 1; + tracing::warn!(correlation_id = ?None::, error = ?e, "failed to parse message"); + crate::metrics::inc_deser_errors(); + if *deser_failures >= MAX_DESER_FAILURES { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "too many deserialization failures", + )); + } + return Ok(()); + } + Err(EnvelopeDecodeError::Deserialize(e)) => { *deser_failures += 1; tracing::warn!(correlation_id = ?None::, error = ?e, "failed to deserialize message"); crate::metrics::inc_deser_errors(); diff --git a/src/app/envelope.rs b/src/app/envelope.rs index f76e504d..d963e510 100644 --- a/src/app/envelope.rs +++ b/src/app/envelope.rs @@ -1,7 +1,8 @@ //! Packet abstraction and envelope types. //! -//! This module defines the [`Packet`] trait along with [`Envelope`] and -//! [`PacketParts`] used to decompose and reassemble messages. +//! These types decouple serialisation from routing by wrapping raw payloads in +//! identifiers understood by [`crate::app::WireframeApp`]. Applications can +//! inspect metadata before deserialising full messages. use crate::message::Message; @@ -56,7 +57,7 @@ pub trait Packet: Message + Send + Sync + 'static { } /// Component values extracted from or used to build a [`Packet`]. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct PacketParts { id: u32, correlation_id: Option, @@ -67,7 +68,7 @@ pub struct PacketParts { /// /// Incoming frames are deserialized into an `Envelope` containing the /// message identifier and raw payload bytes. -#[derive(bincode::Decode, bincode::Encode, Debug)] +#[derive(bincode::Decode, bincode::Encode, Debug, Clone)] pub struct Envelope { pub(crate) id: u32, pub(crate) correlation_id: Option, @@ -133,23 +134,27 @@ impl PacketParts { /// ``` #[must_use] pub fn inherit_correlation(mut self, source: Option) -> Self { - match (self.correlation_id, source) { - (None, cid) => self.correlation_id = cid, - (Some(cid), Some(src)) if cid != src => { - tracing::warn!( - id = self.id, - expected = src, - found = cid, - "mismatched correlation id in response", - ); - // Overwrite with the source correlation ID to ensure downstream - // consistency. - self.correlation_id = Some(src); - } - _ => {} + let (next, mismatched) = Self::select_correlation(self.correlation_id, source); + if mismatched && let (Some(found), Some(expected)) = (self.correlation_id, next) { + tracing::warn!( + id = self.id, + expected, + found, + "mismatched correlation id in response", + ); } + self.correlation_id = next; self } + + #[inline] + fn select_correlation(current: Option, source: Option) -> (Option, bool) { + match (current, source) { + (None, cid) => (cid, false), + (Some(cid), Some(src)) if cid != src => (Some(src), true), + (curr, _) => (curr, false), + } + } } impl From for PacketParts { diff --git a/src/app/error.rs b/src/app/error.rs index 315ce024..10dc337c 100644 --- a/src/app/error.rs +++ b/src/app/error.rs @@ -1,43 +1,28 @@ //! Error types for application setup and messaging. -use tokio::io; +use std::io; + +use thiserror::Error; /// Top-level error type for application setup. -#[derive(Debug)] +#[derive(Debug, Error, PartialEq, Eq)] +#[non_exhaustive] pub enum WireframeError { /// A route with the provided identifier was already registered. + #[error("route id {0} was already registered")] DuplicateRoute(u32), } /// Errors produced when sending a handler response over a stream. -#[derive(Debug)] +#[derive(Debug, Error)] +#[non_exhaustive] pub enum SendError { - /// Serialization failed. - Serialize(Box), + /// Serialisation failed. + #[error("serialisation error: {0}")] + Serialize(#[source] Box), /// Writing to the stream failed. - Io(io::Error), -} - -impl std::fmt::Display for SendError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SendError::Serialize(e) => write!(f, "serialization error: {e}"), - SendError::Io(e) => write!(f, "I/O error: {e}"), - } - } -} - -impl std::error::Error for SendError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - SendError::Serialize(e) => Some(&**e), - SendError::Io(e) => Some(e), - } - } -} - -impl From for SendError { - fn from(e: io::Error) -> Self { SendError::Io(e) } + #[error("I/O error: {0}")] + Io(#[from] io::Error), } /// Result type used throughout the builder API. diff --git a/src/app/mod.rs b/src/app/mod.rs index caa92ff2..1fc12f50 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,4 +1,13 @@ -//! Application builder and related types. +//! Application layer surface: builder, connection handling, envelope and error types. +//! +//! This module curates and re-exports the primary application APIs so downstream +//! crates can `use wireframe::app::*` to access: +//! - [`WireframeApp`] and builder traits ([`Handler`], [`Middleware`], [`ConnectionSetup`], +//! [`ConnectionTeardown`]) +//! - Envelope primitives ([`Envelope`], [`Packet`], [`PacketParts`]) +//! - Error and result types ([`WireframeError`], [`SendError`], [`Result`]) +//! +//! See the `examples/` directory for end-to-end usage. mod builder; mod connection; diff --git a/tests/metadata.rs b/tests/metadata.rs index f1af3de2..05b09fb6 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -20,9 +20,8 @@ fn mock_wireframe_app_with_serializer(serializer: S) -> TestApp where S: TestSerializer, { - TestApp::::new() + TestApp::with_serializer(serializer) .expect("failed to create app") - .serializer(serializer) .route(1, Arc::new(|_| Box::pin(async {}))) .expect("route registration failed") } From c6bc202c9b903b701db9993cee390d4dfe29985a Mon Sep 17 00:00:00 2001 From: Leynos Date: Wed, 27 Aug 2025 12:59:52 +0100 Subject: [PATCH 05/15] Refine connection handling and docs --- examples/metadata_routing.rs | 1 + src/app/builder.rs | 32 +++++++--------- src/app/connection.rs | 35 +++++++++++------ src/app/envelope.rs | 28 +++++++++----- tests/common/mod.rs | 5 ++- tests/metadata.rs | 4 +- tests/response.rs | 14 +++---- wireframe_testing/src/helpers.rs | 64 ++++++++++++++++---------------- 8 files changed, 100 insertions(+), 83 deletions(-) diff --git a/examples/metadata_routing.rs b/examples/metadata_routing.rs index 68c7fc24..91a9fd15 100644 --- a/examples/metadata_routing.rs +++ b/examples/metadata_routing.rs @@ -17,6 +17,7 @@ use wireframe::{ type App = wireframe::app::WireframeApp; /// Frame format with a two-byte id, one-byte flags, and bincode payload. +#[derive(Default)] struct HeaderSerializer; impl Serializer for HeaderSerializer { diff --git a/src/app/builder.rs b/src/app/builder.rs index c7568c3e..e705e900 100644 --- a/src/app/builder.rs +++ b/src/app/builder.rs @@ -69,7 +69,11 @@ pub struct WireframeApp< pub(super) handlers: HashMap>, pub(super) routes: OnceCell>>>, pub(super) middleware: Vec, Output = HandlerService>>>, - #[allow(dead_code)] + #[allow( + dead_code, + reason = "Deprecated: retained temporarily for API compatibility until codec-based \ + framing is fully removed" + )] pub(super) frame_processor: Box, Error = io::Error> + Send + Sync>, pub(super) serializer: S, @@ -143,7 +147,7 @@ where /// /// ``` /// use wireframe::app::WireframeApp; - /// WireframeApp::<_, _, wireframe::app::Envelope>::new().unwrap(); + /// WireframeApp::<_, _, wireframe::app::Envelope>::new().expect("failed to initialise app"); /// ``` pub fn new() -> Result { Ok(Self::default()) } @@ -161,7 +165,7 @@ where impl WireframeApp where - S: Serializer + Send + Sync, + S: Serializer + Default + Send + Sync, C: Send + 'static, E: Packet, { @@ -173,20 +177,8 @@ where /// forward compatibility. pub fn with_serializer(serializer: S) -> Result { Ok(Self { - handlers: HashMap::new(), - routes: OnceCell::new(), - middleware: Vec::new(), - frame_processor: Box::new(crate::frame::LengthPrefixedProcessor::new( - crate::frame::LengthFormat::default(), - )), serializer, - app_data: HashMap::new(), - on_connect: None, - on_disconnect: None, - protocol: None, - push_dlq: None, - buffer_capacity: 1024, - read_timeout_ms: 100, + ..Self::default() }) } @@ -313,7 +305,9 @@ where /// use tokio::sync::mpsc; /// use wireframe::app::WireframeApp; /// - /// # fn build() -> WireframeApp { WireframeApp::new().unwrap() } + /// # fn build() -> WireframeApp { + /// # WireframeApp::new().expect("builder creation should not fail") + /// # } /// # fn main() { /// let (tx, _rx) = mpsc::channel(16); /// let app = build().with_push_dlq(tx); @@ -335,7 +329,7 @@ where pub fn protocol( &self, ) -> Option, ProtocolError = ()>>> { - self.protocol.as_ref().map(Arc::clone) + self.protocol.clone() } /// Return protocol hooks derived from the installed protocol. @@ -345,7 +339,7 @@ where pub fn protocol_hooks(&self) -> ProtocolHooks, ()> { self.protocol .as_ref() - .map(|p| ProtocolHooks::from_protocol(&Arc::clone(p))) + .map(ProtocolHooks::from_protocol) .unwrap_or_default() } diff --git a/src/app/connection.rs b/src/app/connection.rs index 18d11d99..dbf1c45d 100644 --- a/src/app/connection.rs +++ b/src/app/connection.rs @@ -28,7 +28,7 @@ const MAX_DESER_FAILURES: u32 = 10; #[derive(Debug)] enum EnvelopeDecodeError { Parse(E), - Deserialize(Box), + Deserialize(String), } impl WireframeApp @@ -83,15 +83,15 @@ where .or_else(|_| { self.serializer .deserialize::(frame) - .map_err(EnvelopeDecodeError::Deserialize) + .map_err(|e| EnvelopeDecodeError::Deserialize(e.to_string())) }) } - /// Handle an accepted connection. + /// Handle an accepted connection end-to-end. /// - /// This placeholder immediately closes the connection after the - /// preamble phase. A warning is logged so tests and callers are - /// aware of the current limitation. + /// Runs optional connection setup to produce per-connection state, + /// initialises (and caches) route chains, processes the framed stream + /// with per-frame timeouts, and finally runs optional teardown. pub async fn handle_connection(&self, stream: W) where W: AsyncRead + AsyncWrite + Send + Unpin + 'static, @@ -143,10 +143,18 @@ where let mut deser_failures = 0u32; let timeout_dur = Duration::from_millis(self.read_timeout_ms); - while let Ok(Some(frame)) = timeout(timeout_dur, framed.next()).await { - let buf = frame?; - self.handle_frame(&mut framed, buf.as_ref(), &mut deser_failures, routes) - .await?; + loop { + match timeout(timeout_dur, framed.next()).await { + Ok(Some(Ok(buf))) => { + self.handle_frame(&mut framed, buf.as_ref(), &mut deser_failures, routes) + .await?; + } + Ok(Some(Err(e))) => return Err(e), + Ok(None) => break, + Err(_) => { + tracing::debug!("read timeout elapsed; continuing to wait for next frame"); + } + } } Ok(()) @@ -231,8 +239,11 @@ where } } } else { - tracing::warn!(id = env.id, correlation_id = ?env.correlation_id, "no handler for message id"); - crate::metrics::inc_handler_errors(); + tracing::warn!( + id = env.id, + correlation_id = ?env.correlation_id, + "no handler for message id" + ); } Ok(()) diff --git a/src/app/envelope.rs b/src/app/envelope.rs index d963e510..36e17b33 100644 --- a/src/app/envelope.rs +++ b/src/app/envelope.rs @@ -1,8 +1,10 @@ //! Packet abstraction and envelope types. //! //! These types decouple serialisation from routing by wrapping raw payloads in -//! identifiers understood by [`crate::app::WireframeApp`]. Applications can -//! inspect metadata before deserialising full messages. +//! identifiers understood by [`crate::app::WireframeApp`]. This allows the +//! builder (`crate::app::WireframeApp`) to route frames before full +//! deserialisation. See [`crate::app::builder::WireframeApp`] for how envelopes +//! are used when registering routes. use crate::message::Message; @@ -66,7 +68,7 @@ pub struct PacketParts { /// Basic envelope type used by [`WireframeApp::handle_connection`]. /// -/// Incoming frames are deserialized into an `Envelope` containing the +/// Incoming frames are deserialised into an `Envelope` containing the /// message identifier and raw payload bytes. #[derive(bincode::Decode, bincode::Encode, Debug, Clone)] pub struct Envelope { @@ -133,15 +135,21 @@ impl PacketParts { /// assert_eq!(parts.correlation_id(), Some(8)); /// ``` #[must_use] + #[expect( + clippy::collapsible_if, + reason = "avoid if-let chain until it is considered stable" + )] pub fn inherit_correlation(mut self, source: Option) -> Self { let (next, mismatched) = Self::select_correlation(self.correlation_id, source); - if mismatched && let (Some(found), Some(expected)) = (self.correlation_id, next) { - tracing::warn!( - id = self.id, - expected, - found, - "mismatched correlation id in response", - ); + if mismatched { + if let (Some(found), Some(expected)) = (self.correlation_id, next) { + tracing::warn!( + id = self.id, + expected, + found, + "mismatched correlation id in response", + ); + } } self.correlation_id = next; self diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0d177f8c..3462b915 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -7,6 +7,7 @@ use std::net::{Ipv4Addr, SocketAddr, TcpListener as StdTcpListener}; /// Create a TCP listener bound to a free local port. +#[allow(dead_code)] pub fn unused_listener() -> StdTcpListener { let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0); StdTcpListener::bind(addr).expect("failed to bind port") @@ -15,7 +16,7 @@ pub fn unused_listener() -> StdTcpListener { use rstest::fixture; use wireframe::{app::Envelope, serializer::BincodeSerializer}; -type TestApp = wireframe::app::WireframeApp; +pub type TestApp = wireframe::app::WireframeApp; #[fixture] #[allow( @@ -23,5 +24,5 @@ type TestApp = wireframe::app::WireframeApp; reason = "rustc false positive for single line rstest fixtures" )] pub fn factory() -> impl Fn() -> TestApp + Send + Sync + Clone + 'static { - || TestApp::new().expect("WireframeApp::new failed") + || TestApp::new().expect("TestApp::new failed") } diff --git a/tests/metadata.rs b/tests/metadata.rs index 05b09fb6..bc24b387 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -18,7 +18,7 @@ type TestApp = wireframe::app::WireframeApp(serializer: S) -> TestApp where - S: TestSerializer, + S: TestSerializer + Default, { TestApp::with_serializer(serializer) .expect("failed to create app") @@ -26,6 +26,7 @@ where .expect("route registration failed") } +#[derive(Default)] struct CountingSerializer(Arc); impl Serializer for CountingSerializer { @@ -69,6 +70,7 @@ async fn metadata_parser_invoked_before_deserialize() { assert_eq!(counter.load(Ordering::SeqCst), 1); } +#[derive(Default)] struct FallbackSerializer(Arc, Arc); impl Serializer for FallbackSerializer { diff --git a/tests/response.rs b/tests/response.rs index 8596b899..30298828 100644 --- a/tests/response.rs +++ b/tests/response.rs @@ -6,13 +6,13 @@ use bytes::BytesMut; use rstest::rstest; use wireframe::{ - app::Envelope, frame::{Endianness, FrameProcessor, LengthFormat, LengthPrefixedProcessor}, message::Message, serializer::BincodeSerializer, }; -type TestApp = wireframe::app::WireframeApp; +mod common; +use common::TestApp; #[derive(bincode::Encode, bincode::BorrowDecode, PartialEq, Debug)] struct TestResp(u32); @@ -37,9 +37,9 @@ impl<'de> bincode::BorrowDecode<'de, ()> for FailingResp { } } +/// Tests that sending a response serializes and frames the data correctly, +/// and that the response can be decoded and deserialized back to its original value asynchronously. #[tokio::test] -/// Tests that sending a response serialises and frames the data correctly, -/// and that the response can be decoded and deserialised back to its original value asynchronously. async fn send_response_encodes_and_frames() { let app = TestApp::new() .expect("failed to create app") @@ -60,11 +60,11 @@ async fn send_response_encodes_and_frames() { assert_eq!(decoded, TestResp(7)); } -#[tokio::test] /// Tests that decoding with an incomplete length prefix header returns `None` and does not consume /// any bytes from the buffer. /// /// This ensures that the decoder waits for the full header before attempting to decode a frame. +#[tokio::test] async fn length_prefixed_decode_requires_complete_header() { let processor = LengthPrefixedProcessor::default(); let mut buf = BytesMut::from(&[0x00, 0x00, 0x00][..]); // only 3 bytes @@ -72,11 +72,11 @@ async fn length_prefixed_decode_requires_complete_header() { assert_eq!(buf.len(), 3); // nothing consumed } -#[tokio::test] /// Tests that decoding with a complete length prefix but incomplete frame data returns `None` /// and retains all bytes in the buffer. /// /// Ensures that the decoder does not consume any bytes when the full frame is not yet available. +#[tokio::test] async fn length_prefixed_decode_requires_full_frame() { let processor = LengthPrefixedProcessor::default(); let mut buf = BytesMut::from(&[0x00, 0x00, 0x00, 0x05, 0x01, 0x02][..]); @@ -183,11 +183,11 @@ fn encode_fails_for_length_too_large(#[case] fmt: LengthFormat, #[case] len: usi assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); } -#[tokio::test] /// Tests that `send_response` returns a serialization error when encoding fails. /// /// This test sends a `FailingResp` using `send_response` and asserts that the resulting /// error is of the `Serialize` variant, indicating a failure during response encoding. +#[tokio::test] async fn send_response_returns_encode_error() { let app = TestApp::new().expect("failed to create app"); let err = app diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index b3ae67ca..6d16fac7 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -158,10 +158,10 @@ macro_rules! forward_with_capacity { /// duplex stream. /// /// ```rust -/// # use wireframe_testing::{drive_with_frame, processor}; +/// # use wireframe_testing::drive_with_frame; /// # use wireframe::app::WireframeApp; -/// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().unwrap(); +/// # async fn demo() -> std::io::Result<()> { +/// let app = WireframeApp::new().expect("failed to initialise app"); /// let bytes = drive_with_frame(app, vec![1, 2, 3]).await?; /// # Ok(()) /// # } @@ -184,10 +184,10 @@ forward_with_capacity! { /// Adjusting the buffer size helps exercise edge cases such as small channels. /// /// ```rust - /// # use wireframe_testing::{drive_with_frame_with_capacity, processor}; + /// # use wireframe_testing::drive_with_frame_with_capacity; /// # use wireframe::app::WireframeApp; - /// # async fn demo() -> tokio::io::Result<()> { - /// let app = WireframeApp::new().unwrap(); + /// # async fn demo() -> std::io::Result<()> { + /// let app = WireframeApp::new().expect("failed to initialise app"); /// let bytes = drive_with_frame_with_capacity(app, vec![0], 512).await?; /// # Ok(()) /// # } @@ -202,10 +202,10 @@ forward_default! { /// Each frame is written to the duplex stream in order. /// /// ```rust - /// # use wireframe_testing::{drive_with_frames, processor}; + /// # use wireframe_testing::drive_with_frames; /// # use wireframe::app::WireframeApp; - /// # async fn demo() -> tokio::io::Result<()> { - /// let app = WireframeApp::new().unwrap(); + /// # async fn demo() -> std::io::Result<()> { + /// let app = WireframeApp::new().expect("failed to initialise app"); /// let out = drive_with_frames(app, vec![vec![1], vec![2]]).await?; /// # Ok(()) /// # } @@ -219,10 +219,10 @@ forward_default! { /// This variant exposes the buffer size for fine-grained control in tests. /// /// ```rust -/// # use wireframe_testing::{drive_with_frames_with_capacity, processor}; +/// # use wireframe_testing::drive_with_frames_with_capacity; /// # use wireframe::app::WireframeApp; -/// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().unwrap(); +/// # async fn demo() -> std::io::Result<()> { +/// let app = WireframeApp::new().expect("failed to initialise app"); /// let out = drive_with_frames_with_capacity(app, vec![vec![1], vec![2]], 1024).await?; /// # Ok(()) /// # } @@ -250,10 +250,10 @@ forward_default! { /// across calls. /// /// ```rust - /// # use wireframe_testing::{drive_with_frame_mut, processor}; + /// # use wireframe_testing::drive_with_frame_mut; /// # use wireframe::app::WireframeApp; - /// # async fn demo() -> tokio::io::Result<()> { - /// let mut app = WireframeApp::new().unwrap(); + /// # async fn demo() -> std::io::Result<()> { + /// let mut app = WireframeApp::new().expect("failed to initialise app"); /// let bytes = drive_with_frame_mut(&mut app, vec![1]).await?; /// # Ok(()) /// # } @@ -266,10 +266,10 @@ forward_with_capacity! { /// Feed a single frame into `app` using a duplex buffer of `capacity` bytes. /// /// ```rust - /// # use wireframe_testing::{drive_with_frame_with_capacity_mut, processor}; + /// # use wireframe_testing::drive_with_frame_with_capacity_mut; /// # use wireframe::app::WireframeApp; - /// # async fn demo() -> tokio::io::Result<()> { - /// let mut app = WireframeApp::new().unwrap(); + /// # async fn demo() -> std::io::Result<()> { + /// let mut app = WireframeApp::new().expect("failed to initialise app"); /// let bytes = drive_with_frame_with_capacity_mut(&mut app, vec![1], 256).await?; /// # Ok(()) /// # } @@ -282,10 +282,10 @@ forward_default! { /// Feed multiple frames into a mutable `app`. /// /// ```rust - /// # use wireframe_testing::{drive_with_frames_mut, processor}; + /// # use wireframe_testing::drive_with_frames_mut; /// # use wireframe::app::WireframeApp; - /// # async fn demo() -> tokio::io::Result<()> { - /// let mut app = WireframeApp::new().unwrap(); + /// # async fn demo() -> std::io::Result<()> { + /// let mut app = WireframeApp::new().expect("failed to initialise app"); /// let out = drive_with_frames_mut(&mut app, vec![vec![1], vec![2]]).await?; /// # Ok(()) /// # } @@ -297,10 +297,10 @@ forward_default! { /// Feed multiple frames into `app` with a duplex buffer of `capacity` bytes. /// /// ```rust -/// # use wireframe_testing::{drive_with_frames_with_capacity_mut, processor}; +/// # use wireframe_testing::drive_with_frames_with_capacity_mut; /// # use wireframe::app::WireframeApp; -/// # async fn demo() -> tokio::io::Result<()> { -/// let mut app = WireframeApp::new().unwrap(); +/// # async fn demo() -> std::io::Result<()> { +/// let mut app = WireframeApp::new().expect("failed to initialise app"); /// let out = drive_with_frames_with_capacity_mut(&mut app, vec![vec![1], vec![2]], 64).await?; /// # Ok(()) /// # } @@ -326,12 +326,12 @@ where /// Encode `msg` using bincode, frame it and drive `app`. /// /// ```rust -/// # use wireframe_testing::{drive_with_bincode, processor}; +/// # use wireframe_testing::drive_with_bincode; /// # use wireframe::app::WireframeApp; /// #[derive(bincode::Encode)] /// struct Ping(u8); -/// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().unwrap(); +/// # async fn demo() -> std::io::Result<()> { +/// let app = WireframeApp::new().expect("failed to initialise app"); /// let bytes = drive_with_bincode(app, Ping(1)).await?; /// # Ok(()) /// # } @@ -370,10 +370,10 @@ where /// surfaced as an error. /// /// ```rust -/// # use wireframe_testing::{processor, run_app}; +/// # use wireframe_testing::run_app; /// # use wireframe::app::WireframeApp; -/// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().unwrap(); +/// # async fn demo() -> std::io::Result<()> { +/// let app = WireframeApp::new().expect("failed to initialise app"); /// let out = run_app(app, vec![vec![1]], None).await?; /// # Ok(()) /// # } @@ -433,11 +433,11 @@ where /// Panics if `handle_connection` fails. /// /// ```rust -/// # use wireframe_testing::{run_with_duplex_server, processor}; +/// # use wireframe_testing::run_with_duplex_server; /// # use wireframe::app::WireframeApp; /// # async fn demo() { /// let app = WireframeApp::new() -/// .unwrap(); +/// .expect("failed to initialise app"); /// run_with_duplex_server(app).await; /// } /// ``` From a80d0f8b34c2a2aff1fea377525502871a94207a Mon Sep 17 00:00:00 2001 From: Leynos Date: Wed, 27 Aug 2025 12:59:58 +0100 Subject: [PATCH 06/15] Simplify serializer override --- src/app/builder.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/builder.rs b/src/app/builder.rs index e705e900..25a36bfc 100644 --- a/src/app/builder.rs +++ b/src/app/builder.rs @@ -175,11 +175,14 @@ where /// /// This function currently never returns an error but uses [`Result`] for /// forward compatibility. + #[allow( + clippy::field_reassign_with_default, + reason = "overriding serializer post-default simplifies builder" + )] pub fn with_serializer(serializer: S) -> Result { - Ok(Self { - serializer, - ..Self::default() - }) + let mut app = Self::default(); + app.serializer = serializer; + Ok(app) } /// Register a route that maps `id` to `handler`. From 4bdab2d8c254004f7710b16b7c2b99365db4f8f6 Mon Sep 17 00:00:00 2001 From: Leynos Date: Wed, 27 Aug 2025 22:06:46 +0100 Subject: [PATCH 07/15] Drop duplex client in test helper --- wireframe_testing/src/helpers.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index 6d16fac7..c3c745c4 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -447,7 +447,8 @@ where C: Send + 'static, E: Packet, { - let (_client, server) = duplex(64); + let (client, server) = duplex(64); + drop(client); app.handle_connection(server).await; } From ec91e2b25bcbe2b8ab010918cda3ad702e1f61bb Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 28 Aug 2025 09:53:01 +0100 Subject: [PATCH 08/15] Drop duplex client half in test helper --- wireframe_testing/src/helpers.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index c3c745c4..3ca6b37a 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -448,6 +448,7 @@ where E: Packet, { let (client, server) = duplex(64); + // Drop the client end immediately so the server sees EOF. drop(client); app.handle_connection(server).await; } From 64e6b6a00d374be745484fb5e586e02e72730979 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 28 Aug 2025 10:18:56 +0100 Subject: [PATCH 09/15] Clamp builder configuration and tidy tests --- src/app/builder.rs | 11 +++++++---- src/app/mod.rs | 7 +++---- tests/metadata.rs | 5 +++-- tests/response.rs | 5 +---- wireframe_testing/src/helpers.rs | 5 ++--- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/app/builder.rs b/src/app/builder.rs index 25a36bfc..6efb0cde 100644 --- a/src/app/builder.rs +++ b/src/app/builder.rs @@ -381,16 +381,19 @@ where } } - /// Set the initial buffer capacity for framed reads (clamped to ≥64). + /// Set the initial buffer capacity for framed reads. + /// Clamped between 64 bytes and 16 MiB. #[must_use] pub fn buffer_capacity(mut self, capacity: usize) -> Self { - self.buffer_capacity = capacity.max(64); + self.buffer_capacity = capacity.clamp(64, 16 * 1024 * 1024); self } - /// Configure the read timeout in milliseconds (clamped to ≥1). + + /// Configure the read timeout in milliseconds. + /// Clamped between 1 and 86 400 000 milliseconds (24 h). #[must_use] pub fn read_timeout_ms(mut self, timeout_ms: u64) -> Self { - self.read_timeout_ms = timeout_ms.max(1); + self.read_timeout_ms = timeout_ms.clamp(1, 86_400_000); self } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 1fc12f50..8815855c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,11 +1,10 @@ -//! Application layer surface: builder, connection handling, envelope and error types. +//! Application builder and supporting types. //! -//! This module curates and re-exports the primary application APIs so downstream -//! crates can `use wireframe::app::*` to access: +//! Re-exports: //! - [`WireframeApp`] and builder traits ([`Handler`], [`Middleware`], [`ConnectionSetup`], //! [`ConnectionTeardown`]) //! - Envelope primitives ([`Envelope`], [`Packet`], [`PacketParts`]) -//! - Error and result types ([`WireframeError`], [`SendError`], [`Result`]) +//! - Error handling types ([`WireframeError`], [`SendError`], [`Result`]) //! //! See the `examples/` directory for end-to-end usage. diff --git a/tests/metadata.rs b/tests/metadata.rs index bc24b387..de2af122 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -18,12 +18,13 @@ type TestApp = wireframe::app::WireframeApp(serializer: S) -> TestApp where - S: TestSerializer + Default, + S: TestSerializer, { - TestApp::with_serializer(serializer) + wireframe::app::WireframeApp::::new() .expect("failed to create app") .route(1, Arc::new(|_| Box::pin(async {}))) .expect("route registration failed") + .serializer(serializer) } #[derive(Default)] diff --git a/tests/response.rs b/tests/response.rs index 30298828..92e355de 100644 --- a/tests/response.rs +++ b/tests/response.rs @@ -8,7 +8,6 @@ use rstest::rstest; use wireframe::{ frame::{Endianness, FrameProcessor, LengthFormat, LengthPrefixedProcessor}, message::Message, - serializer::BincodeSerializer, }; mod common; @@ -41,9 +40,7 @@ impl<'de> bincode::BorrowDecode<'de, ()> for FailingResp { /// and that the response can be decoded and deserialized back to its original value asynchronously. #[tokio::test] async fn send_response_encodes_and_frames() { - let app = TestApp::new() - .expect("failed to create app") - .serializer(BincodeSerializer); + let app = TestApp::new().expect("failed to create app"); let mut out = Vec::new(); app.send_response(&mut out, &TestResp(7)) diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index 3ca6b37a..28db5b2e 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -97,6 +97,7 @@ where const DEFAULT_CAPACITY: usize = 4096; const MAX_CAPACITY: usize = 1024 * 1024 * 10; // 10MB limit +pub(crate) const EMPTY_SERVER_CAPACITY: usize = 64; macro_rules! forward_default { ( @@ -447,9 +448,7 @@ where C: Send + 'static, E: Packet, { - let (client, server) = duplex(64); - // Drop the client end immediately so the server sees EOF. - drop(client); + let (_, server) = duplex(EMPTY_SERVER_CAPACITY); // discard client half app.handle_connection(server).await; } From 99cc36b8bf43d1ef60754d38a98bfdc8cdd04d15 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 28 Aug 2025 10:42:36 +0100 Subject: [PATCH 10/15] Use std::io in test helper docs --- wireframe_testing/src/helpers.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index 28db5b2e..d0d1270e 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -3,10 +3,12 @@ //! These functions spin up an application on an in-memory duplex stream and //! collect the bytes written back by the app for assertions. +use std::io; + use bincode::config; use bytes::BytesMut; use rstest::fixture; -use tokio::io::{self, AsyncReadExt, AsyncWriteExt, DuplexStream, duplex}; +use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream, duplex}; use wireframe::{ app::{Envelope, Packet, WireframeApp}, frame::{FrameMetadata, FrameProcessor, LengthPrefixedProcessor}, @@ -15,10 +17,8 @@ use wireframe::{ /// Create a default length-prefixed frame processor for tests. #[fixture] -#[allow( - unused_braces, - reason = "Clippy is wrong here; this is not a redundant block" -)] +#[expect(unused_braces, reason = "Braces are intentional here; false positive")] +#[allow(unfulfilled_lint_expectations)] pub fn processor() -> LengthPrefixedProcessor { LengthPrefixedProcessor::default() } pub trait TestSerializer: @@ -41,12 +41,12 @@ impl TestSerializer for T where /// with `"server task failed"`. /// /// ```rust -/// use tokio::io::{self, AsyncWriteExt, DuplexStream}; +/// use tokio::io::{AsyncWriteExt, DuplexStream}; /// use wireframe_testing::helpers::drive_internal; /// /// async fn echo(mut server: DuplexStream) { let _ = server.write_all(&[1, 2]).await; } /// -/// # async fn demo() -> io::Result<()> { +/// # async fn demo() -> std::io::Result<()> { /// let bytes = drive_internal(echo, vec![vec![0]], 64).await?; /// assert_eq!(bytes, [1, 2]); /// # Ok(()) From 0d2ac01323e69465daefb4fc01b4ff546ac09ccd Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 28 Aug 2025 11:56:33 +0100 Subject: [PATCH 11/15] Address review feedback on examples and tests --- Cargo.toml | 12 ++++++++++-- examples/metadata_routing.rs | 4 ++-- examples/packet_enum.rs | 15 ++++++++------- examples/ping_pong.rs | 5 ++++- src/app/mod.rs | 2 +- src/lib.rs | 3 +++ tests/lifecycle.rs | 2 +- tests/metadata.rs | 12 ++++++------ tests/response.rs | 5 ++++- tests/world.rs | 13 ++++++------- 10 files changed, 45 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bd16be1c..1e4e3b76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ tokio = { version = "1.46.1", default-features = false, features = [ "sync", "time", "io-util", - "test-util", ] } tokio-util = { version = "0.7.16", features = ["rt", "codec"] } futures = "0.3.31" @@ -50,10 +49,19 @@ serial_test = "3.2.0" # Permit compatible bug fixes but block breaking updates cucumber = "0.21.1" metrics-util = "0.20.0" -tracing = { version = "0.1.41", features = ["log", "log-always"] } tracing-test = "0.2.5" mockall = "0.13.1" +tokio = { version = "1.46.1", default-features = false, features = [ + "macros", + "rt-multi-thread", + "sync", + "time", + "io-util", + "net", + "test-util", +] } + [features] default = ["metrics"] metrics = ["dep:metrics", "dep:metrics-exporter-prometheus"] diff --git a/examples/metadata_routing.rs b/examples/metadata_routing.rs index 91a9fd15..1cb24f18 100644 --- a/examples/metadata_routing.rs +++ b/examples/metadata_routing.rs @@ -71,7 +71,7 @@ async fn main() -> io::Result<()> { 1, Arc::new(|_env: &Envelope| { Box::pin(async move { - println!("received ping message"); + tracing::info!("received ping message"); }) }), ) @@ -80,7 +80,7 @@ async fn main() -> io::Result<()> { 2, Arc::new(|_env: &Envelope| { Box::pin(async move { - println!("received pong message"); + tracing::info!("received pong message"); }) }), ) diff --git a/examples/packet_enum.rs b/examples/packet_enum.rs index 92f2f808..544fe5b4 100644 --- a/examples/packet_enum.rs +++ b/examples/packet_enum.rs @@ -6,6 +6,7 @@ use std::{collections::HashMap, future::Future, pin::Pin}; use async_trait::async_trait; +use tracing::{info, warn}; use wireframe::{ app::Envelope, message::Message, @@ -17,7 +18,7 @@ use wireframe::{ type App = wireframe::app::WireframeApp; #[derive(bincode::Encode, bincode::BorrowDecode, Debug)] -enum Packet { +enum ExamplePacket { Ping, Chat { user: String, msg: String }, Stats(Vec), @@ -26,7 +27,7 @@ enum Packet { #[derive(bincode::Encode, bincode::BorrowDecode, Debug)] struct Frame { headers: HashMap, - packet: Packet, + packet: ExamplePacket, } /// Middleware that decodes incoming frames and logs packet details. @@ -47,12 +48,12 @@ where async fn call(&self, req: ServiceRequest) -> Result { match Frame::from_bytes(req.frame()) { Ok((frame, _)) => match frame.packet { - Packet::Ping => println!("ping: {:?}", frame.headers), - Packet::Chat { user, msg } => println!("{user} says: {msg}"), - Packet::Stats(values) => println!("stats: {values:?}"), + ExamplePacket::Ping => info!("ping: {:?}", frame.headers), + ExamplePacket::Chat { user, msg } => info!("{user} says: {msg}"), + ExamplePacket::Stats(values) => info!("stats: {values:?}"), }, Err(e) => { - eprintln!("Failed to decode frame: {e}"); + warn!("Failed to decode frame: {e}"); } } @@ -73,7 +74,7 @@ impl Transform> for DecodeMiddleware { fn handle_packet(_env: &Envelope) -> Pin + Send>> { Box::pin(async { - println!("packet received"); + info!("packet received"); }) } diff --git a/examples/ping_pong.rs b/examples/ping_pong.rs index 9d9810ad..7410d31b 100644 --- a/examples/ping_pong.rs +++ b/examples/ping_pong.rs @@ -42,7 +42,10 @@ const PING_ID: u32 = 1; /// /// The middleware chain generates the actual response, so this /// handler intentionally performs no work. -#[allow(clippy::unused_async)] +#[expect( + clippy::unused_async, + reason = "Keep async signature to match Handler and Transform trait expectations" +)] async fn ping_handler() {} struct PongMiddleware; diff --git a/src/app/mod.rs b/src/app/mod.rs index 8815855c..02268ceb 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -11,7 +11,7 @@ mod builder; mod connection; mod envelope; -mod error; +pub mod error; pub use builder::{ConnectionSetup, ConnectionTeardown, Handler, Middleware, WireframeApp}; pub use envelope::{Envelope, Packet, PacketParts}; diff --git a/src/lib.rs b/src/lib.rs index 2ae0d074..368302d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,9 @@ //! servers, including routing, middleware, and connection utilities. pub mod app; +/// Result type alias re-exported for convenience when working with the +/// application builder. +pub use app::error::Result; pub mod serializer; pub use serializer::{BincodeSerializer, Serializer}; pub mod connection; diff --git a/tests/lifecycle.rs b/tests/lifecycle.rs index ba2810a3..709a6e14 100644 --- a/tests/lifecycle.rs +++ b/tests/lifecycle.rs @@ -131,7 +131,7 @@ impl Packet for StateEnvelope { } #[tokio::test] -async fn helpers_propagate_connection_state() { +async fn helpers_preserve_correlation_id_and_run_callbacks() { let setup = Arc::new(AtomicUsize::new(0)); let teardown = Arc::new(AtomicUsize::new(0)); diff --git a/tests/metadata.rs b/tests/metadata.rs index de2af122..e5cf8c19 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -51,7 +51,7 @@ impl FrameMetadata for CountingSerializer { type Error = bincode::error::DecodeError; fn parse(&self, src: &[u8]) -> Result<(Self::Frame, usize), Self::Error> { - self.0.fetch_add(1, Ordering::SeqCst); + self.0.fetch_add(1, Ordering::Relaxed); BincodeSerializer.parse(src) } } @@ -68,7 +68,7 @@ async fn metadata_parser_invoked_before_deserialize() { .await .expect("drive_with_bincode failed"); assert!(!out.is_empty()); - assert_eq!(counter.load(Ordering::SeqCst), 1); + assert_eq!(counter.load(Ordering::Relaxed), 1); } #[derive(Default)] @@ -86,7 +86,7 @@ impl Serializer for FallbackSerializer { &self, bytes: &[u8], ) -> Result<(M, usize), Box> { - self.1.fetch_add(1, Ordering::SeqCst); + self.1.fetch_add(1, Ordering::Relaxed); BincodeSerializer.deserialize(bytes) } } @@ -96,7 +96,7 @@ impl FrameMetadata for FallbackSerializer { type Error = bincode::error::DecodeError; fn parse(&self, _src: &[u8]) -> Result<(Self::Frame, usize), Self::Error> { - self.0.fetch_add(1, Ordering::SeqCst); + self.0.fetch_add(1, Ordering::Relaxed); Err(bincode::error::DecodeError::OtherString("fail".into())) } } @@ -114,6 +114,6 @@ async fn falls_back_to_deserialize_after_parse_error() { .await .expect("drive_with_bincode failed"); assert!(!out.is_empty()); - assert_eq!(parse_calls.load(Ordering::SeqCst), 1); - assert_eq!(deser_calls.load(Ordering::SeqCst), 1); + assert_eq!(parse_calls.load(Ordering::Relaxed), 1); + assert_eq!(deser_calls.load(Ordering::Relaxed), 1); } diff --git a/tests/response.rs b/tests/response.rs index 92e355de..41d484c4 100644 --- a/tests/response.rs +++ b/tests/response.rs @@ -6,8 +6,10 @@ use bytes::BytesMut; use rstest::rstest; use wireframe::{ + app::{Envelope, WireframeApp}, frame::{Endianness, FrameProcessor, LengthFormat, LengthPrefixedProcessor}, message::Message, + serializer::BincodeSerializer, }; mod common; @@ -186,7 +188,8 @@ fn encode_fails_for_length_too_large(#[case] fmt: LengthFormat, #[case] len: usi /// error is of the `Serialize` variant, indicating a failure during response encoding. #[tokio::test] async fn send_response_returns_encode_error() { - let app = TestApp::new().expect("failed to create app"); + // Intentionally do not set a frame processor: encode should fail before framing. + let app = WireframeApp::::new().expect("failed to create app"); let err = app .send_response(&mut Vec::new(), &FailingResp) .await diff --git a/tests/world.rs b/tests/world.rs index 303e5f31..c36ab604 100644 --- a/tests/world.rs +++ b/tests/world.rs @@ -73,18 +73,17 @@ impl PanicServer { impl Drop for PanicServer { fn drop(&mut self) { - use std::time::Duration; + use std::{thread, time::Duration}; if let Some(tx) = self.shutdown.take() { let _ = tx.send(()); } let timeout = Duration::from_secs(5); - let joined = futures::executor::block_on(tokio::time::timeout(timeout, &mut self.handle)); - match joined { - Ok(Ok(())) => {} - Ok(Err(e)) => eprintln!("PanicServer task panicked: {e:?}"), - Err(_) => eprintln!("PanicServer task did not shut down within timeout"), - } + let handle = self.handle.abort_handle(); + thread::spawn(move || { + thread::sleep(timeout); + handle.abort(); + }); } } From 1875fab23e149facbdcbaff52d207a252d9f5894 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 28 Aug 2025 11:59:27 +0100 Subject: [PATCH 12/15] Use Middleware alias and standardize initialization docs --- src/app/builder.rs | 9 +++++---- src/app/connection.rs | 2 +- src/app/envelope.rs | 20 +++++++------------- tests/metadata.rs | 5 ++--- wireframe_testing/src/helpers.rs | 22 +++++++++++----------- 5 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/app/builder.rs b/src/app/builder.rs index 6efb0cde..b27eeaca 100644 --- a/src/app/builder.rs +++ b/src/app/builder.rs @@ -69,11 +69,12 @@ pub struct WireframeApp< pub(super) handlers: HashMap>, pub(super) routes: OnceCell>>>, pub(super) middleware: Vec, Output = HandlerService>>>, - #[allow( + #[expect( dead_code, reason = "Deprecated: retained temporarily for API compatibility until codec-based \ - framing is fully removed" + framing is fully removed", )] + #[allow(unfulfilled_lint_expectations)] pub(super) frame_processor: Box, Error = io::Error> + Send + Sync>, pub(super) serializer: S, @@ -147,7 +148,7 @@ where /// /// ``` /// use wireframe::app::WireframeApp; - /// WireframeApp::<_, _, wireframe::app::Envelope>::new().expect("failed to initialise app"); + /// WireframeApp::<_, _, wireframe::app::Envelope>::new().expect("failed to initialize app"); /// ``` pub fn new() -> Result { Ok(Self::default()) } @@ -223,7 +224,7 @@ where /// This function currently always succeeds. pub fn wrap(mut self, mw: M) -> Result where - M: Transform, Output = HandlerService> + Send + Sync + 'static, + M: Middleware + 'static, { self.middleware.push(Box::new(mw)); self.routes = OnceCell::new(); diff --git a/src/app/connection.rs b/src/app/connection.rs index dbf1c45d..916e90cf 100644 --- a/src/app/connection.rs +++ b/src/app/connection.rs @@ -90,7 +90,7 @@ where /// Handle an accepted connection end-to-end. /// /// Runs optional connection setup to produce per-connection state, - /// initialises (and caches) route chains, processes the framed stream + /// initializes (and caches) route chains, processes the framed stream /// with per-frame timeouts, and finally runs optional teardown. pub async fn handle_connection(&self, stream: W) where diff --git a/src/app/envelope.rs b/src/app/envelope.rs index 36e17b33..cd5eda1c 100644 --- a/src/app/envelope.rs +++ b/src/app/envelope.rs @@ -135,21 +135,15 @@ impl PacketParts { /// assert_eq!(parts.correlation_id(), Some(8)); /// ``` #[must_use] - #[expect( - clippy::collapsible_if, - reason = "avoid if-let chain until it is considered stable" - )] pub fn inherit_correlation(mut self, source: Option) -> Self { let (next, mismatched) = Self::select_correlation(self.correlation_id, source); - if mismatched { - if let (Some(found), Some(expected)) = (self.correlation_id, next) { - tracing::warn!( - id = self.id, - expected, - found, - "mismatched correlation id in response", - ); - } + if mismatched && let (Some(found), Some(expected)) = (self.correlation_id, next) { + tracing::warn!( + id = self.id, + expected, + found, + "mismatched correlation id in response", + ); } self.correlation_id = next; self diff --git a/tests/metadata.rs b/tests/metadata.rs index e5cf8c19..c30b4a47 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -18,13 +18,12 @@ type TestApp = wireframe::app::WireframeApp(serializer: S) -> TestApp where - S: TestSerializer, + S: TestSerializer + Default, { - wireframe::app::WireframeApp::::new() + wireframe::app::WireframeApp::::with_serializer(serializer) .expect("failed to create app") .route(1, Arc::new(|_| Box::pin(async {}))) .expect("route registration failed") - .serializer(serializer) } #[derive(Default)] diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index d0d1270e..08f232a2 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -162,7 +162,7 @@ macro_rules! forward_with_capacity { /// # use wireframe_testing::drive_with_frame; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> std::io::Result<()> { -/// let app = WireframeApp::new().expect("failed to initialise app"); +/// let app = WireframeApp::new().expect("failed to initialize app"); /// let bytes = drive_with_frame(app, vec![1, 2, 3]).await?; /// # Ok(()) /// # } @@ -188,7 +188,7 @@ forward_with_capacity! { /// # use wireframe_testing::drive_with_frame_with_capacity; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> std::io::Result<()> { - /// let app = WireframeApp::new().expect("failed to initialise app"); + /// let app = WireframeApp::new().expect("failed to initialize app"); /// let bytes = drive_with_frame_with_capacity(app, vec![0], 512).await?; /// # Ok(()) /// # } @@ -206,7 +206,7 @@ forward_default! { /// # use wireframe_testing::drive_with_frames; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> std::io::Result<()> { - /// let app = WireframeApp::new().expect("failed to initialise app"); + /// let app = WireframeApp::new().expect("failed to initialize app"); /// let out = drive_with_frames(app, vec![vec![1], vec![2]]).await?; /// # Ok(()) /// # } @@ -223,7 +223,7 @@ forward_default! { /// # use wireframe_testing::drive_with_frames_with_capacity; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> std::io::Result<()> { -/// let app = WireframeApp::new().expect("failed to initialise app"); +/// let app = WireframeApp::new().expect("failed to initialize app"); /// let out = drive_with_frames_with_capacity(app, vec![vec![1], vec![2]], 1024).await?; /// # Ok(()) /// # } @@ -254,7 +254,7 @@ forward_default! { /// # use wireframe_testing::drive_with_frame_mut; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> std::io::Result<()> { - /// let mut app = WireframeApp::new().expect("failed to initialise app"); + /// let mut app = WireframeApp::new().expect("failed to initialize app"); /// let bytes = drive_with_frame_mut(&mut app, vec![1]).await?; /// # Ok(()) /// # } @@ -270,7 +270,7 @@ forward_with_capacity! { /// # use wireframe_testing::drive_with_frame_with_capacity_mut; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> std::io::Result<()> { - /// let mut app = WireframeApp::new().expect("failed to initialise app"); + /// let mut app = WireframeApp::new().expect("failed to initialize app"); /// let bytes = drive_with_frame_with_capacity_mut(&mut app, vec![1], 256).await?; /// # Ok(()) /// # } @@ -286,7 +286,7 @@ forward_default! { /// # use wireframe_testing::drive_with_frames_mut; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> std::io::Result<()> { - /// let mut app = WireframeApp::new().expect("failed to initialise app"); + /// let mut app = WireframeApp::new().expect("failed to initialize app"); /// let out = drive_with_frames_mut(&mut app, vec![vec![1], vec![2]]).await?; /// # Ok(()) /// # } @@ -301,7 +301,7 @@ forward_default! { /// # use wireframe_testing::drive_with_frames_with_capacity_mut; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> std::io::Result<()> { -/// let mut app = WireframeApp::new().expect("failed to initialise app"); +/// let mut app = WireframeApp::new().expect("failed to initialize app"); /// let out = drive_with_frames_with_capacity_mut(&mut app, vec![vec![1], vec![2]], 64).await?; /// # Ok(()) /// # } @@ -332,7 +332,7 @@ where /// #[derive(bincode::Encode)] /// struct Ping(u8); /// # async fn demo() -> std::io::Result<()> { -/// let app = WireframeApp::new().expect("failed to initialise app"); +/// let app = WireframeApp::new().expect("failed to initialize app"); /// let bytes = drive_with_bincode(app, Ping(1)).await?; /// # Ok(()) /// # } @@ -374,7 +374,7 @@ where /// # use wireframe_testing::run_app; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> std::io::Result<()> { -/// let app = WireframeApp::new().expect("failed to initialise app"); +/// let app = WireframeApp::new().expect("failed to initialize app"); /// let out = run_app(app, vec![vec![1]], None).await?; /// # Ok(()) /// # } @@ -438,7 +438,7 @@ where /// # use wireframe::app::WireframeApp; /// # async fn demo() { /// let app = WireframeApp::new() -/// .expect("failed to initialise app"); +/// .expect("failed to initialize app"); /// run_with_duplex_server(app).await; /// } /// ``` From e1c4c73005ed36cfdedfec569ebdf957e3cc8181 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 28 Aug 2025 12:28:01 +0100 Subject: [PATCH 13/15] Scope lint expectations --- src/app/builder.rs | 4 ++-- tests/common/mod.rs | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/builder.rs b/src/app/builder.rs index b27eeaca..69b9b2dd 100644 --- a/src/app/builder.rs +++ b/src/app/builder.rs @@ -72,7 +72,7 @@ pub struct WireframeApp< #[expect( dead_code, reason = "Deprecated: retained temporarily for API compatibility until codec-based \ - framing is fully removed", + framing is fully removed" )] #[allow(unfulfilled_lint_expectations)] pub(super) frame_processor: @@ -176,7 +176,7 @@ where /// /// This function currently never returns an error but uses [`Result`] for /// forward compatibility. - #[allow( + #[expect( clippy::field_reassign_with_default, reason = "overriding serializer post-default simplifies builder" )] diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 3462b915..16c83285 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -7,7 +7,8 @@ use std::net::{Ipv4Addr, SocketAddr, TcpListener as StdTcpListener}; /// Create a TCP listener bound to a free local port. -#[allow(dead_code)] +#[expect(dead_code, reason = "Used by tests that bind to random ports")] +#[allow(unfulfilled_lint_expectations)] pub fn unused_listener() -> StdTcpListener { let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0); StdTcpListener::bind(addr).expect("failed to bind port") @@ -19,10 +20,11 @@ use wireframe::{app::Envelope, serializer::BincodeSerializer}; pub type TestApp = wireframe::app::WireframeApp; #[fixture] -#[allow( +#[expect( unused_braces, reason = "rustc false positive for single line rstest fixtures" )] +#[allow(unfulfilled_lint_expectations)] pub fn factory() -> impl Fn() -> TestApp + Send + Sync + Clone + 'static { || TestApp::new().expect("TestApp::new failed") } From 7821c791f551c0d18e1915600df1080744cd43a7 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 28 Aug 2025 16:58:23 +0100 Subject: [PATCH 14/15] Refine builder bounds and framed responses --- examples/metadata_routing.rs | 12 +++++++++--- src/app/builder.rs | 22 ++++++++++++---------- src/app/connection.rs | 25 +++++++++++++++++++++++-- src/app/envelope.rs | 4 ++-- 4 files changed, 46 insertions(+), 17 deletions(-) diff --git a/examples/metadata_routing.rs b/examples/metadata_routing.rs index 1cb24f18..6d4ea7b6 100644 --- a/examples/metadata_routing.rs +++ b/examples/metadata_routing.rs @@ -11,15 +11,19 @@ use wireframe::{ app::Envelope, frame::{FrameMetadata, FrameProcessor, LengthPrefixedProcessor}, message::Message, - serializer::{BincodeSerializer, Serializer}, + serializer::Serializer, }; -type App = wireframe::app::WireframeApp; +type App = wireframe::app::WireframeApp; /// Frame format with a two-byte id, one-byte flags, and bincode payload. #[derive(Default)] struct HeaderSerializer; +impl HeaderSerializer { + fn with_serializer(_inner: S) -> Self { Self } +} + impl Serializer for HeaderSerializer { fn serialize( &self, @@ -66,7 +70,9 @@ struct Ping; async fn main() -> io::Result<()> { let app = App::new() .expect("failed to create app") - .serializer(HeaderSerializer) + .serializer(HeaderSerializer::with_serializer( + wireframe::serializer::BincodeSerializer, + )) .route( 1, Arc::new(|_env: &Envelope| { diff --git a/src/app/builder.rs b/src/app/builder.rs index 69b9b2dd..3e18f327 100644 --- a/src/app/builder.rs +++ b/src/app/builder.rs @@ -27,6 +27,11 @@ use crate::{ middleware::{HandlerService, Transform}, serializer::{BincodeSerializer, Serializer}, }; + +const MIN_BUFFER_CAP: usize = 64; +const MAX_BUFFER_CAP: usize = 16 * 1024 * 1024; +const MIN_READ_TIMEOUT_MS: u64 = 1; +const MAX_READ_TIMEOUT_MS: u64 = 86_400_000; /// Callback invoked when a connection is established. /// /// # Examples @@ -68,7 +73,7 @@ pub struct WireframeApp< > { pub(super) handlers: HashMap>, pub(super) routes: OnceCell>>>, - pub(super) middleware: Vec, Output = HandlerService>>>, + pub(super) middleware: Vec>>, #[expect( dead_code, reason = "Deprecated: retained temporarily for API compatibility until codec-based \ @@ -176,14 +181,11 @@ where /// /// This function currently never returns an error but uses [`Result`] for /// forward compatibility. - #[expect( - clippy::field_reassign_with_default, - reason = "overriding serializer post-default simplifies builder" - )] pub fn with_serializer(serializer: S) -> Result { - let mut app = Self::default(); - app.serializer = serializer; - Ok(app) + Ok(Self { + serializer, + ..Self::default() + }) } /// Register a route that maps `id` to `handler`. @@ -386,7 +388,7 @@ where /// Clamped between 64 bytes and 16 MiB. #[must_use] pub fn buffer_capacity(mut self, capacity: usize) -> Self { - self.buffer_capacity = capacity.clamp(64, 16 * 1024 * 1024); + self.buffer_capacity = capacity.clamp(MIN_BUFFER_CAP, MAX_BUFFER_CAP); self } @@ -394,7 +396,7 @@ where /// Clamped between 1 and 86 400 000 milliseconds (24 h). #[must_use] pub fn read_timeout_ms(mut self, timeout_ms: u64) -> Self { - self.read_timeout_ms = timeout_ms.clamp(1, 86_400_000); + self.read_timeout_ms = timeout_ms.clamp(MIN_READ_TIMEOUT_MS, MAX_READ_TIMEOUT_MS); self } } diff --git a/src/app/connection.rs b/src/app/connection.rs index 916e90cf..04947571 100644 --- a/src/app/connection.rs +++ b/src/app/connection.rs @@ -28,7 +28,7 @@ const MAX_DESER_FAILURES: u32 = 10; #[derive(Debug)] enum EnvelopeDecodeError { Parse(E), - Deserialize(String), + Deserialize(Box), } impl WireframeApp @@ -63,6 +63,27 @@ where stream.write_all(&framed).await.map_err(SendError::Io)?; stream.flush().await.map_err(SendError::Io) } + + /// Serialize `msg` and send it through an existing framed stream. + /// + /// # Errors + /// + /// Returns a [`SendError`] if serialization or sending fails. + pub async fn send_response_framed( + &self, + framed: &mut Framed, + msg: &M, + ) -> std::result::Result<(), SendError> + where + W: AsyncRead + AsyncWrite + Unpin, + M: Message, + { + let bytes = self + .serializer + .serialize(msg) + .map_err(SendError::Serialize)?; + framed.send(bytes.into()).await.map_err(SendError::Io) + } } impl WireframeApp @@ -83,7 +104,7 @@ where .or_else(|_| { self.serializer .deserialize::(frame) - .map_err(|e| EnvelopeDecodeError::Deserialize(e.to_string())) + .map_err(EnvelopeDecodeError::Deserialize) }) } diff --git a/src/app/envelope.rs b/src/app/envelope.rs index cd5eda1c..f138769e 100644 --- a/src/app/envelope.rs +++ b/src/app/envelope.rs @@ -59,7 +59,7 @@ pub trait Packet: Message + Send + Sync + 'static { } /// Component values extracted from or used to build a [`Packet`]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PacketParts { id: u32, correlation_id: Option, @@ -70,7 +70,7 @@ pub struct PacketParts { /// /// Incoming frames are deserialised into an `Envelope` containing the /// message identifier and raw payload bytes. -#[derive(bincode::Decode, bincode::Encode, Debug, Clone)] +#[derive(bincode::Decode, bincode::Encode, Debug, Clone, PartialEq, Eq)] pub struct Envelope { pub(crate) id: u32, pub(crate) correlation_id: Option, From fa32270fb0f26cf84ad385b67dd7e6abd96daa11 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 28 Aug 2025 18:01:42 +0100 Subject: [PATCH 15/15] Use header serializer directly in metadata example --- examples/metadata_routing.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/examples/metadata_routing.rs b/examples/metadata_routing.rs index 6d4ea7b6..d23b6222 100644 --- a/examples/metadata_routing.rs +++ b/examples/metadata_routing.rs @@ -14,16 +14,12 @@ use wireframe::{ serializer::Serializer, }; -type App = wireframe::app::WireframeApp; +type App = wireframe::app::WireframeApp; /// Frame format with a two-byte id, one-byte flags, and bincode payload. #[derive(Default)] struct HeaderSerializer; -impl HeaderSerializer { - fn with_serializer(_inner: S) -> Self { Self } -} - impl Serializer for HeaderSerializer { fn serialize( &self, @@ -68,11 +64,8 @@ struct Ping; #[tokio::main] async fn main() -> io::Result<()> { - let app = App::new() + let app = App::with_serializer(HeaderSerializer) .expect("failed to create app") - .serializer(HeaderSerializer::with_serializer( - wireframe::serializer::BincodeSerializer, - )) .route( 1, Arc::new(|_env: &Envelope| {