dstar_gateway_core/error/
state.rs

1//! `StateError` for runtime state-machine residuals.
2
3use crate::session::client::ClientStateKind;
4use crate::types::{ProtocolKind, StreamId};
5
6/// Runtime state-machine errors that the typestate cannot prevent.
7#[derive(Debug, Clone, thiserror::Error)]
8#[non_exhaustive]
9pub enum StateError {
10    /// `send_header` for stream X while stream Y is still streaming.
11    #[error("stream {already_active} is still streaming; cannot start {requested}")]
12    StreamAlreadyActive {
13        /// The stream id currently in flight.
14        already_active: StreamId,
15        /// The stream id the caller tried to start.
16        requested: StreamId,
17    },
18
19    /// `send_voice` with seq > 20 (D-STAR seq is mod 21).
20    #[error("voice sequence {got} out of range; D-STAR seq must be 0..21")]
21    VoiceSeqOutOfRange {
22        /// The rejected seq value.
23        got: u8,
24    },
25
26    /// Receive callback fired for a stream id with no known session state.
27    #[error("received frame for unknown stream id {stream_id}")]
28    UnknownStreamId {
29        /// The stream id with no matching session.
30        stream_id: StreamId,
31    },
32
33    /// A method was called on a [`SessionCore`] while it was in the
34    /// wrong [`ClientStateKind`] — e.g. `enqueue_connect` outside of
35    /// `Configured`/`Authenticated`, or `attach_host_list` on a
36    /// non-`DPlus` session.
37    ///
38    /// The typestate wrapper ([`crate::session::client::Session`]) prevents
39    /// this at compile time; the variant exists so direct `SessionCore`
40    /// users (tests + the protocol-erased fallback) get a useful
41    /// runtime error instead of a panic.
42    ///
43    /// [`SessionCore`]: crate::session::client::SessionCore
44    #[error("{operation} is not valid in {state:?} for protocol {protocol:?}")]
45    WrongState {
46        /// Which operation was attempted.
47        operation: &'static str,
48        /// The current runtime state.
49        state: ClientStateKind,
50        /// The runtime protocol discriminator.
51        protocol: ProtocolKind,
52    },
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    const SID_1111: StreamId = StreamId::new(0x1111).unwrap();
60    const SID_2222: StreamId = StreamId::new(0x2222).unwrap();
61
62    #[test]
63    fn state_error_stream_already_active_display() {
64        let err = StateError::StreamAlreadyActive {
65            already_active: SID_1111,
66            requested: SID_2222,
67        };
68        let s = err.to_string();
69        assert!(s.contains("0x1111"));
70        assert!(s.contains("0x2222"));
71    }
72
73    #[test]
74    fn state_error_voice_seq_out_of_range_display() {
75        let err = StateError::VoiceSeqOutOfRange { got: 25 };
76        assert!(err.to_string().contains("25"));
77    }
78
79    #[test]
80    fn state_error_wrong_state_display() {
81        let err = StateError::WrongState {
82            operation: "enqueue_connect",
83            state: ClientStateKind::Connected,
84            protocol: ProtocolKind::DPlus,
85        };
86        let s = err.to_string();
87        assert!(s.contains("enqueue_connect"));
88        assert!(s.contains("Connected"));
89        assert!(s.contains("DPlus"));
90    }
91}