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}