dstar_gateway_core/session/server/
session.rs

1//! Typestate `ServerSession<P, S>` wrapper.
2//!
3//! Thin wrapper over [`ServerSessionCore`] that adds compile-time
4//! state discrimination via the `S: ServerState` phantom. The real
5//! state transitions happen on the erased core, and this wrapper
6//! provides universal accessors that work in any state plus
7//! state-gated methods that only resolve in the correct state.
8
9use std::marker::PhantomData;
10use std::net::SocketAddr;
11
12use super::core::ServerSessionCore;
13use super::event::ServerEvent;
14use super::state::{Closed, Link1Received, Linked, ServerState, ServerStateKind, Streaming};
15
16use crate::error::Error;
17use crate::session::client::{DPlus, Protocol};
18use crate::types::Callsign;
19
20/// A typed server-side reflector session.
21///
22/// `P` is the protocol marker ([`crate::session::client::DExtra`],
23/// [`crate::session::client::DPlus`], [`crate::session::client::Dcs`]).
24/// `S` is the server state marker ([`super::Unknown`],
25/// [`super::Link1Received`], [`super::Linked`], etc.).
26#[derive(Debug)]
27pub struct ServerSession<P: Protocol, S: ServerState> {
28    pub(crate) inner: ServerSessionCore,
29    pub(crate) _protocol: PhantomData<P>,
30    pub(crate) _state: PhantomData<S>,
31}
32
33// ─── Universal: state inspection works in any state ──────────────
34
35impl<P: Protocol, S: ServerState> ServerSession<P, S> {
36    /// Runtime state discriminator.
37    #[must_use]
38    pub const fn state_kind(&self) -> ServerStateKind {
39        self.inner.state_kind()
40    }
41
42    /// Peer address.
43    #[must_use]
44    pub const fn peer(&self) -> SocketAddr {
45        self.inner.peer()
46    }
47
48    /// Drain the next server event.
49    pub fn pop_event(&mut self) -> Option<ServerEvent<P>> {
50        self.inner.pop_event::<P>()
51    }
52}
53
54// ─── State-gated methods ──────────────────────────────────────────
55//
56// Each of the following `impl` blocks bounds one state marker. A
57// call site in the wrong state will fail to compile with an
58// `E0599: no method found` error — the compile-fail trybuild tests
59// prove the gates are tight.
60//
61// The bodies here are intentionally thin wrappers over the
62// protocol-erased core; the real state machine lives in `core.rs`.
63// Each method's purpose is to assert which state it's callable in
64// and to consume the session on terminal transitions.
65
66impl<P: Protocol> ServerSession<P, Streaming> {
67    /// Process a voice data packet on a streaming client.
68    ///
69    /// Only available when `S = Streaming` — calling this in any
70    /// other state is a compile error.
71    ///
72    /// # Errors
73    ///
74    /// Returns any [`Error`] the core propagates while processing
75    /// the event.
76    pub fn handle_voice_data(
77        mut self,
78        now: std::time::Instant,
79        bytes: &[u8],
80    ) -> Result<Self, Error> {
81        self.inner.handle_input(now, bytes)?;
82        Ok(self)
83    }
84}
85
86impl ServerSession<DPlus, Link1Received> {
87    /// Process a `DPlus` LINK2 packet after a LINK1 has been
88    /// received.
89    ///
90    /// Only available on `ServerSession<DPlus, Link1Received>` —
91    /// calling this before LINK1 (in the [`super::Unknown`] state)
92    /// is a compile error.
93    ///
94    /// # Errors
95    ///
96    /// Returns any [`Error`] the core propagates.
97    pub fn handle_link2(
98        mut self,
99        now: std::time::Instant,
100        _callsign: Callsign,
101        bytes: &[u8],
102    ) -> Result<ServerSession<DPlus, Linked>, Error> {
103        self.inner.handle_input(now, bytes)?;
104        Ok(ServerSession {
105            inner: self.inner,
106            _protocol: PhantomData,
107            _state: PhantomData,
108        })
109    }
110}
111
112impl<P: Protocol> ServerSession<P, Linked> {
113    /// Process an UNLINK packet on a linked client.
114    ///
115    /// Only available on `ServerSession<P, Linked>` — calling this
116    /// after the session has already reached [`Closed`] is a
117    /// compile error.
118    ///
119    /// # Errors
120    ///
121    /// Returns any [`Error`] the core propagates.
122    pub fn handle_unlink(
123        mut self,
124        now: std::time::Instant,
125        bytes: &[u8],
126    ) -> Result<ServerSession<P, Closed>, Error> {
127        self.inner.handle_input(now, bytes)?;
128        Ok(ServerSession {
129            inner: self.inner,
130            _protocol: PhantomData,
131            _state: PhantomData,
132        })
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::super::core::ServerSessionCore;
139    use super::super::state::{ServerStateKind, Unknown};
140    use super::ServerSession;
141    use crate::session::client::DExtra;
142    use crate::types::{Module, ProtocolKind};
143    use std::marker::PhantomData;
144    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
145
146    const PEER: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 30001);
147
148    #[test]
149    fn universal_accessors_work() {
150        let inner = ServerSessionCore::new(ProtocolKind::DExtra, PEER, Module::C);
151        let session: ServerSession<DExtra, Unknown> = ServerSession {
152            inner,
153            _protocol: PhantomData,
154            _state: PhantomData,
155        };
156        assert_eq!(session.state_kind(), ServerStateKind::Unknown);
157        assert_eq!(session.peer(), PEER);
158    }
159
160    #[test]
161    fn pop_event_returns_none_on_fresh_session() {
162        let inner = ServerSessionCore::new(ProtocolKind::DExtra, PEER, Module::C);
163        let mut session: ServerSession<DExtra, Unknown> = ServerSession {
164            inner,
165            _protocol: PhantomData,
166            _state: PhantomData,
167        };
168        assert!(session.pop_event().is_none());
169    }
170}