Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/builder-pattern-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Builder pattern conventions

This guide describes how to handle type-transitioning builder methods in
Wireframe. When a method changes a generic parameter, struct update syntax
(`..self`) cannot be used, so the builder must be reconstructed explicitly.

## Choosing an approach

Use a helper method when:

- The builder has many fields (roughly ten or more).
- Type transitions update multiple related fields.
- The same reconstruction logic appears in more than one method.

Use a macro when:

- The builder has a small, stable field set (single digits).
- Each method updates a single field.
- The reconstruction is a straightforward field copy.

## Current patterns

- `WireframeApp::rebuild_with_params` centralizes reconstruction for the
13-field server builder and keeps coordinated updates for serializer, codec,
protocol, and fragmentation together.
- `builder_field_update!` in `src/client/builder/mod.rs` covers the five-field
client builder, where each type change updates one field at a time.
2 changes: 2 additions & 0 deletions docs/contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ the-road-to-wireframe-1-0-feature-set-philosophy-and-capability-maturity.md

- [Refactoring guide](complexity-antipatterns-and-refactoring-strategies.md)
Strategies for taming code complexity and refactoring.
- [Builder pattern conventions](builder-pattern-conventions.md) Guidance for
type-transitioning builders and reconstruction patterns.
- [Documentation style guide](documentation-style-guide.md) Conventions for
writing project documentation.
- [Server configuration](server/configuration.md) Tuning accept loop backoff
Expand Down
14 changes: 10 additions & 4 deletions src/app/builder/codec.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Codec and serializer configuration for `WireframeApp`.

use super::WireframeApp;
use super::{WireframeApp, core::RebuildParams};
use crate::{
app::Packet,
codec::{FrameCodec, LengthDelimitedFrameCodec, clamp_frame_length},
Expand All @@ -26,7 +26,13 @@ where
{
let serializer = std::mem::take(&mut self.serializer);
let message_assembler = self.message_assembler.take();
self.rebuild_with_params(serializer, codec, None, None, message_assembler)
self.rebuild_with_params(RebuildParams {
serializer,
codec,
protocol: None,
fragmentation: None,
message_assembler,
})
}

/// Replace the serializer used for messages.
Expand All @@ -40,13 +46,13 @@ where
let protocol = self.protocol.take();
let fragmentation = self.fragmentation.take();
let message_assembler = self.message_assembler.take();
self.rebuild_with_params(
self.rebuild_with_params(RebuildParams {
serializer,
codec,
protocol,
fragmentation,
message_assembler,
)
})
}
}

Expand Down
51 changes: 28 additions & 23 deletions src/app/builder/core.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
//! Core builder types for `WireframeApp`.

use std::{
any::{Any, TypeId},
collections::HashMap,
sync::Arc,
};
use std::{collections::HashMap, sync::Arc};

use tokio::sync::{OnceCell, mpsc};

Expand All @@ -16,6 +12,7 @@ use crate::{
lifecycle::{ConnectionSetup, ConnectionTeardown},
middleware_types::{Handler, Middleware},
},
app_data_store::AppDataStore,
codec::{FrameCodec, LengthDelimitedFrameCodec},
hooks::WireframeProtocol,
message_assembler::MessageAssembler,
Expand All @@ -38,7 +35,7 @@ pub struct WireframeApp<
pub(in crate::app) routes: OnceCell<Arc<HashMap<u32, HandlerService<E>>>>,
pub(in crate::app) middleware: Vec<Box<dyn Middleware<E>>>,
pub(in crate::app) serializer: S,
pub(in crate::app) app_data: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
pub(in crate::app) app_data: AppDataStore,
pub(in crate::app) on_connect: Option<Arc<ConnectionSetup<C>>>,
pub(in crate::app) on_disconnect: Option<Arc<ConnectionTeardown<C>>>,
pub(in crate::app) protocol:
Expand Down Expand Up @@ -66,7 +63,7 @@ where
routes: OnceCell::new(),
middleware: Vec::new(),
serializer: S::default(),
app_data: HashMap::new(),
app_data: AppDataStore::default(),
on_connect: None,
on_disconnect: None,
protocol: None,
Expand Down Expand Up @@ -115,6 +112,18 @@ where
}
}

/// Groups the type-changing parameters for [`WireframeApp::rebuild_with_params`].
///
/// Consolidates serializer, codec, protocol, fragmentation, and message
/// assembler into a single value to keep the rebuild signature concise.
pub(super) struct RebuildParams<S2, F2: FrameCodec> {
pub(super) serializer: S2,
pub(super) codec: F2,
pub(super) protocol: Option<Arc<dyn WireframeProtocol<Frame = F2::Frame, ProtocolError = ()>>>,
pub(super) fragmentation: Option<crate::fragment::FragmentationConfig>,
pub(super) message_assembler: Option<Arc<dyn MessageAssembler>>,
}

impl<S, C, E, F> WireframeApp<S, C, E, F>
where
S: Serializer + Send + Sync,
Expand All @@ -124,19 +133,15 @@ where
{
/// Helper to rebuild the app when changing type parameters.
///
/// This centralises the field-by-field reconstruction required when
/// transforming between different serializer or codec types.
#[expect(
clippy::too_many_arguments,
reason = "internal helper grouping fields for type-transitioning builders"
)]
/// The `WireframeApp` builder carries 13 fields that must be moved together
/// when swapping serializer or codec types. Centralising the reconstruction
/// here keeps the transitions consistent and avoids repeating the same
/// field list across each type-changing method. For smaller builders with
/// only a handful of fields and single-field updates, prefer the macro-based
/// pattern used by `WireframeClientBuilder`.
pub(super) fn rebuild_with_params<S2, F2>(
self,
serializer: S2,
codec: F2,
protocol: Option<Arc<dyn WireframeProtocol<Frame = F2::Frame, ProtocolError = ()>>>,
fragmentation: Option<crate::fragment::FragmentationConfig>,
message_assembler: Option<Arc<dyn MessageAssembler>>,
params: RebuildParams<S2, F2>,
) -> WireframeApp<S2, C, E, F2>
where
S2: Serializer + Send + Sync,
Expand All @@ -146,16 +151,16 @@ where
handlers: self.handlers,
routes: OnceCell::new(),
middleware: self.middleware,
serializer,
serializer: params.serializer,
app_data: self.app_data,
on_connect: self.on_connect,
on_disconnect: self.on_disconnect,
protocol,
protocol: params.protocol,
push_dlq: self.push_dlq,
codec,
codec: params.codec,
read_timeout_ms: self.read_timeout_ms,
fragmentation,
message_assembler,
fragmentation: params.fragmentation,
message_assembler: params.message_assembler,
}
}
}
Expand Down
9 changes: 2 additions & 7 deletions src/app/builder/state.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
//! Shared state configuration for `WireframeApp`.

use std::{any::TypeId, sync::Arc};

use super::WireframeApp;
use crate::{app::Packet, codec::FrameCodec, serializer::Serializer};

Expand All @@ -17,14 +15,11 @@ where
/// 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<T>(mut self, state: T) -> Self
pub fn app_data<T>(self, state: T) -> Self
where
T: Send + Sync + 'static,
{
self.app_data.insert(
TypeId::of::<T>(),
Arc::new(state) as Arc<dyn std::any::Any + Send + Sync>,
);
self.app_data.insert(state);
self
}
}
Loading
Loading