dstar_gateway_core/error/
mod.rs

1//! Error types for `dstar-gateway-core`.
2//!
3//! Per-protocol structured error variants are used so that callers
4//! can pattern-match to distinguish I/O failures from timeouts from
5//! protocol errors from type-validation errors.
6
7mod encode;
8mod io_operation;
9mod protocol;
10mod state;
11mod timeout;
12
13pub use crate::codec::dcs::error::DcsError;
14pub use crate::codec::dextra::error::DExtraError;
15pub use crate::codec::dplus::error::DPlusError;
16pub use encode::EncodeError;
17pub use io_operation::IoOperation;
18pub use protocol::ProtocolError;
19pub use state::StateError;
20pub use timeout::TimeoutError;
21
22use crate::types::TypeError;
23
24/// Top-level error type for `dstar-gateway-core`.
25///
26/// All fallible operations in the core return `Result<_, Error>`.
27/// Pattern-match this enum to distinguish I/O failures from
28/// timeouts from protocol errors from type-validation errors.
29#[derive(Debug, thiserror::Error)]
30#[non_exhaustive]
31pub enum Error {
32    /// Underlying I/O failure on a socket or TCP stream.
33    ///
34    /// Only emitted by the shell crates — `dstar-gateway-core` itself
35    /// has no I/O. Carried here for the shell crates' convenience.
36    #[error("I/O error during {operation:?}: {source}")]
37    Io {
38        /// Underlying `std::io::Error`.
39        #[source]
40        source: std::io::Error,
41        /// What kind of I/O the operation was.
42        operation: IoOperation,
43    },
44
45    /// Construction-time validation failure (Callsign, Module, etc.).
46    #[error(transparent)]
47    Type(#[from] TypeError),
48
49    /// A typed deadline-exceeded error.
50    #[error("operation timed out: {0}")]
51    Timeout(#[source] TimeoutError),
52
53    /// Protocol-level error (per-protocol structured cause).
54    #[error(transparent)]
55    Protocol(#[from] ProtocolError),
56
57    /// State-machine residual that the typestate cannot prevent.
58    #[error("session in invalid state: {0}")]
59    State(#[source] StateError),
60}
61
62impl From<TimeoutError> for Error {
63    fn from(value: TimeoutError) -> Self {
64        Self::Timeout(value)
65    }
66}
67
68impl From<StateError> for Error {
69    fn from(value: StateError) -> Self {
70        Self::State(value)
71    }
72}
73
74#[cfg(test)]
75mod top_level_tests {
76    use super::*;
77    use static_assertions::assert_impl_all;
78
79    assert_impl_all!(Error: Send, Sync);
80
81    #[test]
82    fn error_is_send_sync_static() {
83        // The static assertion above is the real test. This runtime
84        // body just exists so cargo test reports a passing test.
85    }
86
87    #[test]
88    fn type_error_into_top_level() {
89        let te = TypeError::InvalidModule { got: '@' };
90        let err: Error = te.into();
91        assert!(matches!(
92            err,
93            Error::Type(TypeError::InvalidModule { got: '@' })
94        ));
95    }
96
97    #[test]
98    fn timeout_into_top_level() {
99        let to = TimeoutError::Disconnect {
100            deadline: std::time::Duration::from_secs(2),
101            elapsed: std::time::Duration::from_secs(2),
102        };
103        let err: Error = to.into();
104        assert!(matches!(err, Error::Timeout(_)));
105    }
106
107    #[test]
108    fn protocol_into_top_level() {
109        let pe = ProtocolError::DPlus(DPlusError::StreamIdZero);
110        let err: Error = pe.into();
111        assert!(matches!(
112            err,
113            Error::Protocol(ProtocolError::DPlus(DPlusError::StreamIdZero))
114        ));
115    }
116}