dstar_gateway_core/session/client/builder.rs
1//! Typed [`SessionBuilder`] for [`Session<P, Configured>`].
2//!
3//! The typestate builder uses marker types to track which required
4//! fields have been set. Each setter consumes `self` and returns a new
5//! [`SessionBuilder`] with the corresponding marker type flipped from
6//! [`Missing`] to [`Provided`]. The [`SessionBuilder::build`] method
7//! is only implemented on
8//! `SessionBuilder<P, Provided, Provided, Provided, Provided>` — a
9//! missing field turns `.build()` into a compile error.
10
11use std::marker::PhantomData;
12use std::net::SocketAddr;
13
14use crate::types::{Callsign, Module};
15
16use super::core::SessionCore;
17use super::protocol::Protocol;
18use super::session::Session;
19use super::state::Configured;
20
21/// Marker indicating a required builder field has NOT been set.
22#[derive(Debug)]
23pub struct Missing;
24
25/// Marker indicating a required builder field HAS been set.
26#[derive(Debug)]
27pub struct Provided;
28
29/// Typestate builder for [`Session<P, Configured>`].
30///
31/// Parameters:
32///
33/// - `P` — protocol marker ([`super::DPlus`], [`super::DExtra`],
34/// [`super::Dcs`])
35/// - `Cs` — [`Missing`] or [`Provided`], tracks whether the callsign
36/// has been set
37/// - `Lm` — tracks local module
38/// - `Rm` — tracks reflector module
39/// - `Pe` — tracks peer address
40///
41/// All four `Missing`/`Provided` type parameters start as
42/// [`Missing`] and must be flipped to [`Provided`] before
43/// [`Self::build`] becomes callable. The phantoms add zero runtime
44/// cost.
45#[derive(Debug)]
46pub struct SessionBuilder<P: Protocol, Cs, Lm, Rm, Pe> {
47 callsign: Option<Callsign>,
48 local_module: Option<Module>,
49 reflector_module: Option<Module>,
50 peer: Option<SocketAddr>,
51 /// Optional reflector callsign — only required for `DCS`
52 /// sessions targeting a non-`DCS001` reflector. `None` means
53 /// `SessionCore` falls back to its `DCS001 ` default (and
54 /// logs a warning if the protocol is `DCS`).
55 reflector_callsign: Option<Callsign>,
56 _protocol: PhantomData<P>,
57 _cs: PhantomData<Cs>,
58 _lm: PhantomData<Lm>,
59 _rm: PhantomData<Rm>,
60 _pe: PhantomData<Pe>,
61}
62
63impl<P: Protocol, Cs, Lm, Rm, Pe> SessionBuilder<P, Cs, Lm, Rm, Pe> {
64 /// Set the station callsign.
65 #[must_use]
66 pub const fn callsign(self, callsign: Callsign) -> SessionBuilder<P, Provided, Lm, Rm, Pe> {
67 SessionBuilder {
68 callsign: Some(callsign),
69 local_module: self.local_module,
70 reflector_module: self.reflector_module,
71 peer: self.peer,
72 reflector_callsign: self.reflector_callsign,
73 _protocol: PhantomData,
74 _cs: PhantomData,
75 _lm: PhantomData,
76 _rm: PhantomData,
77 _pe: PhantomData,
78 }
79 }
80
81 /// Set the local module letter (the module on the client side).
82 #[must_use]
83 pub const fn local_module(self, module: Module) -> SessionBuilder<P, Cs, Provided, Rm, Pe> {
84 SessionBuilder {
85 callsign: self.callsign,
86 local_module: Some(module),
87 reflector_module: self.reflector_module,
88 peer: self.peer,
89 reflector_callsign: self.reflector_callsign,
90 _protocol: PhantomData,
91 _cs: PhantomData,
92 _lm: PhantomData,
93 _rm: PhantomData,
94 _pe: PhantomData,
95 }
96 }
97
98 /// Set the reflector module letter (the module we want to link to).
99 #[must_use]
100 pub const fn reflector_module(self, module: Module) -> SessionBuilder<P, Cs, Lm, Provided, Pe> {
101 SessionBuilder {
102 callsign: self.callsign,
103 local_module: self.local_module,
104 reflector_module: Some(module),
105 peer: self.peer,
106 reflector_callsign: self.reflector_callsign,
107 _protocol: PhantomData,
108 _cs: PhantomData,
109 _lm: PhantomData,
110 _rm: PhantomData,
111 _pe: PhantomData,
112 }
113 }
114
115 /// Set the reflector peer address.
116 #[must_use]
117 pub const fn peer(self, peer: SocketAddr) -> SessionBuilder<P, Cs, Lm, Rm, Provided> {
118 SessionBuilder {
119 callsign: self.callsign,
120 local_module: self.local_module,
121 reflector_module: self.reflector_module,
122 peer: Some(peer),
123 reflector_callsign: self.reflector_callsign,
124 _protocol: PhantomData,
125 _cs: PhantomData,
126 _lm: PhantomData,
127 _rm: PhantomData,
128 _pe: PhantomData,
129 }
130 }
131
132 /// Set the target reflector's own callsign.
133 ///
134 /// **Required for `DCS` sessions targeting any reflector other
135 /// than `DCS001`.** The DCS wire format embeds the reflector
136 /// callsign in every LINK / UNLINK / POLL packet, and a real
137 /// `DCS030` reflector will silently drop traffic whose embedded
138 /// reflector callsign reads `DCS001 `. For `DPlus` and
139 /// `DExtra` this is metadata only — the protocols do not carry
140 /// the reflector callsign on the wire, so the setter is
141 /// harmless when unused.
142 ///
143 /// Unlike the four required fields, this setter does not flip
144 /// a typestate marker — sessions that do not need a reflector
145 /// callsign keep building with four setters, and DCS sessions
146 /// that forget it get a runtime warning at construction time
147 /// plus a `DCS001 ` fallback. Upgrading this to a compile-time
148 /// requirement for `Session<Dcs, _>` specifically is a future
149 /// design refinement.
150 #[must_use]
151 pub const fn reflector_callsign(mut self, reflector_callsign: Callsign) -> Self {
152 self.reflector_callsign = Some(reflector_callsign);
153 self
154 }
155}
156
157impl<P: Protocol> SessionBuilder<P, Provided, Provided, Provided, Provided> {
158 /// Build the [`Session<P, Configured>`].
159 ///
160 /// Only callable when all four required fields have been set —
161 /// any [`Missing`] marker turns this into a compile error.
162 ///
163 /// The [`Provided`] type parameters are the typestate proof that
164 /// every field was set; the `Option` unwrapping below is
165 /// therefore infallible at the type level, and we use
166 /// [`unreachable!`] in the impossible branches rather than
167 /// [`Option::expect`] (which is lint-denied in lib code).
168 #[must_use]
169 pub fn build(self) -> Session<P, Configured> {
170 let Some(callsign) = self.callsign else {
171 unreachable!("Provided marker guarantees callsign is Some");
172 };
173 let Some(local_module) = self.local_module else {
174 unreachable!("Provided marker guarantees local_module is Some");
175 };
176 let Some(reflector_module) = self.reflector_module else {
177 unreachable!("Provided marker guarantees reflector_module is Some");
178 };
179 let Some(peer) = self.peer else {
180 unreachable!("Provided marker guarantees peer is Some");
181 };
182 let core = SessionCore::new_with_reflector_callsign(
183 P::KIND,
184 callsign,
185 local_module,
186 reflector_module,
187 peer,
188 self.reflector_callsign,
189 );
190 Session {
191 inner: core,
192 _protocol: PhantomData,
193 _state: PhantomData,
194 }
195 }
196}
197
198impl<P: Protocol> Session<P, Configured> {
199 /// Entry point for building a typed [`Session<P, Configured>`].
200 ///
201 /// Returns a builder with every required field marked
202 /// [`Missing`]. Chain `.callsign()`, `.local_module()`,
203 /// `.reflector_module()`, and `.peer()` in any order, then call
204 /// `.build()`. Skipping any of the four setters turns the
205 /// `.build()` call into a compile error.
206 ///
207 /// # Example
208 ///
209 /// ```
210 /// use dstar_gateway_core::session::client::{Configured, DExtra, Session};
211 /// use dstar_gateway_core::types::{Callsign, Module};
212 ///
213 /// let session: Session<DExtra, Configured> =
214 /// Session::<DExtra, Configured>::builder()
215 /// .callsign(Callsign::try_from_str("W1AW")?)
216 /// .local_module(Module::try_from_char('B')?)
217 /// .reflector_module(Module::try_from_char('C')?)
218 /// .peer("127.0.0.1:30001".parse()?)
219 /// .build();
220 /// # Ok::<(), Box<dyn std::error::Error>>(())
221 /// ```
222 #[must_use]
223 pub const fn builder() -> SessionBuilder<P, Missing, Missing, Missing, Missing> {
224 SessionBuilder {
225 callsign: None,
226 local_module: None,
227 reflector_module: None,
228 peer: None,
229 reflector_callsign: None,
230 _protocol: PhantomData,
231 _cs: PhantomData,
232 _lm: PhantomData,
233 _rm: PhantomData,
234 _pe: PhantomData,
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::session::client::protocol::{DExtra, DPlus};
243 use crate::session::client::state::ClientStateKind;
244 use std::net::{IpAddr, Ipv4Addr};
245
246 const CS: Callsign = Callsign::from_wire_bytes(*b"W1AW ");
247 const ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 30001);
248 const ADDR_DPLUS: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 20001);
249
250 #[test]
251 fn dextra_builder_happy_path() {
252 let session = Session::<DExtra, Configured>::builder()
253 .callsign(CS)
254 .local_module(Module::B)
255 .reflector_module(Module::C)
256 .peer(ADDR)
257 .build();
258 assert_eq!(session.state_kind(), ClientStateKind::Configured);
259 assert_eq!(session.peer(), ADDR);
260 assert_eq!(session.local_callsign(), CS);
261 }
262
263 #[test]
264 fn dextra_builder_field_order_independent() {
265 // Same as above but setters in a different order.
266 let session = Session::<DExtra, Configured>::builder()
267 .peer(ADDR)
268 .reflector_module(Module::C)
269 .local_module(Module::B)
270 .callsign(CS)
271 .build();
272 assert_eq!(session.state_kind(), ClientStateKind::Configured);
273 }
274
275 #[test]
276 fn dplus_builder_builds_configured() {
277 let session = Session::<DPlus, Configured>::builder()
278 .callsign(CS)
279 .local_module(Module::B)
280 .reflector_module(Module::C)
281 .peer(ADDR_DPLUS)
282 .build();
283 assert_eq!(session.state_kind(), ClientStateKind::Configured);
284 }
285}