Skip to main content

confium_tc_core/
unified_error.rs

1//! Unified error hierarchy — single error tree for all coordinator errors.
2
3use std::fmt;
4
5/// The root error type for the entire confium-tc coordinator.
6#[derive(Debug)]
7pub enum UnifiedError {
8    Session(SessionErrorKind),
9    Policy(PolicyErrorKind),
10    Network(NetworkErrorKind),
11    Crypto(CryptoErrorKind),
12    Store(StoreErrorKind),
13    Config(ConfigErrorKind),
14    RateLimited { retry_after_secs: u64 },
15    Backpressure { active: usize, max: usize },
16    Unauthorized { reason: String },
17    NotFound { resource: String },
18    Internal { message: String },
19}
20
21#[derive(Debug)]
22pub enum SessionErrorKind {
23    NotFound(String),
24    InvalidState {
25        session: String,
26        current: String,
27        expected: String,
28    },
29    ThresholdNotMet {
30        have: usize,
31        need: u32,
32    },
33    DuplicateSubmission {
34        signer: String,
35    },
36    Expired(String),
37    SigningFailed(String),
38}
39
40#[derive(Debug)]
41pub enum PolicyErrorKind {
42    Denied { rule: String, reason: String },
43}
44
45#[derive(Debug)]
46pub enum NetworkErrorKind {
47    ConnectionFailed(String),
48    ProtocolError(String),
49    Timeout,
50}
51
52#[derive(Debug)]
53pub enum CryptoErrorKind {
54    InvalidSignature,
55    InvalidShare,
56    InvalidProof,
57    InvalidKey,
58}
59
60#[derive(Debug)]
61pub enum StoreErrorKind {
62    IoError(String),
63    SerializationError(String),
64    KeyNotFound(String),
65}
66
67#[derive(Debug)]
68pub enum ConfigErrorKind {
69    InvalidValue {
70        field: String,
71        value: String,
72        expected: String,
73    },
74    MissingField(String),
75    FileError(String),
76}
77
78impl fmt::Display for UnifiedError {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Self::Session(e) => write!(f, "session error: {e:?}"),
82            Self::Policy(e) => write!(f, "policy error: {e:?}"),
83            Self::Network(e) => write!(f, "network error: {e:?}"),
84            Self::Crypto(e) => write!(f, "crypto error: {e:?}"),
85            Self::Store(e) => write!(f, "store error: {e:?}"),
86            Self::Config(e) => write!(f, "config error: {e:?}"),
87            Self::RateLimited { retry_after_secs } => {
88                write!(f, "rate limited, retry after {retry_after_secs}s")
89            }
90            Self::Backpressure { active, max } => write!(f, "at capacity: {active}/{max}"),
91            Self::Unauthorized { reason } => write!(f, "unauthorized: {reason}"),
92            Self::NotFound { resource } => write!(f, "not found: {resource}"),
93            Self::Internal { message } => write!(f, "internal: {message}"),
94        }
95    }
96}
97
98impl std::error::Error for UnifiedError {}
99
100/// Error code for programmatic handling.
101impl UnifiedError {
102    pub fn code(&self) -> u32 {
103        match self {
104            Self::Session(SessionErrorKind::NotFound(_)) => 1001,
105            Self::Session(SessionErrorKind::InvalidState { .. }) => 1002,
106            Self::Session(SessionErrorKind::ThresholdNotMet { .. }) => 1003,
107            Self::Session(SessionErrorKind::DuplicateSubmission { .. }) => 1004,
108            Self::Session(SessionErrorKind::Expired(_)) => 1005,
109            Self::Session(SessionErrorKind::SigningFailed(_)) => 1007,
110            Self::Policy(_) => 3001,
111            Self::Network(NetworkErrorKind::ConnectionFailed(_)) => 5001,
112            Self::Network(NetworkErrorKind::ProtocolError(_)) => 5002,
113            Self::Network(NetworkErrorKind::Timeout) => 5003,
114            Self::Crypto(CryptoErrorKind::InvalidSignature) => 4001,
115            Self::Crypto(CryptoErrorKind::InvalidShare) => 4002,
116            Self::Crypto(CryptoErrorKind::InvalidProof) => 4003,
117            Self::Crypto(CryptoErrorKind::InvalidKey) => 4004,
118            Self::Store(_) => 7001,
119            Self::Config(_) => 6001,
120            Self::RateLimited { .. } => 2001,
121            Self::Backpressure { .. } => 2002,
122            Self::Unauthorized { .. } => 1006,
123            Self::NotFound { .. } => 1001,
124            Self::Internal { .. } => 9001,
125        }
126    }
127
128    pub fn is_retryable(&self) -> bool {
129        matches!(
130            self,
131            Self::Session(SessionErrorKind::ThresholdNotMet { .. })
132                | Self::Network(NetworkErrorKind::Timeout)
133                | Self::RateLimited { .. }
134                | Self::Backpressure { .. }
135        )
136    }
137
138    pub fn is_client_error(&self) -> bool {
139        matches!(
140            self,
141            Self::Session(SessionErrorKind::NotFound(_))
142                | Self::Session(SessionErrorKind::DuplicateSubmission { .. })
143                | Self::Session(SessionErrorKind::Expired(_))
144                | Self::Policy(_)
145                | Self::Unauthorized { .. }
146                | Self::NotFound { .. }
147        )
148    }
149
150    pub fn category(&self) -> &'static str {
151        match self {
152            Self::Session(_) => "session",
153            Self::Policy(_) => "policy",
154            Self::Network(_) => "network",
155            Self::Crypto(_) => "crypto",
156            Self::Store(_) => "store",
157            Self::Config(_) => "config",
158            Self::RateLimited { .. } => "rate_limit",
159            Self::Backpressure { .. } => "backpressure",
160            Self::Unauthorized { .. } => "auth",
161            Self::NotFound { .. } => "not_found",
162            Self::Internal { .. } => "internal",
163        }
164    }
165}
166
167// Convenience constructors
168impl UnifiedError {
169    pub fn session_not_found(id: &str) -> Self {
170        Self::Session(SessionErrorKind::NotFound(id.into()))
171    }
172    pub fn rate_limited(retry_after: u64) -> Self {
173        Self::RateLimited {
174            retry_after_secs: retry_after,
175        }
176    }
177    pub fn unauthorized(reason: &str) -> Self {
178        Self::Unauthorized {
179            reason: reason.into(),
180        }
181    }
182    pub fn not_found(resource: &str) -> Self {
183        Self::NotFound {
184            resource: resource.into(),
185        }
186    }
187    pub fn internal(message: &str) -> Self {
188        Self::Internal {
189            message: message.into(),
190        }
191    }
192}
193
194pub type UnifiedResult<T> = Result<T, UnifiedError>;
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn error_display() {
202        let e = UnifiedError::session_not_found("s1");
203        assert!(format!("{e}").contains("s1"));
204    }
205
206    #[test]
207    fn error_code() {
208        assert_eq!(UnifiedError::session_not_found("x").code(), 1001);
209        assert_eq!(UnifiedError::rate_limited(30).code(), 2001);
210        assert_eq!(UnifiedError::internal("test").code(), 9001);
211    }
212
213    #[test]
214    fn retryable_classification() {
215        assert!(UnifiedError::rate_limited(30).is_retryable());
216        assert!(UnifiedError::session_not_found("x").is_client_error());
217        assert!(!UnifiedError::internal("x").is_client_error());
218        assert!(!UnifiedError::session_not_found("x").is_retryable());
219    }
220
221    #[test]
222    fn category() {
223        assert_eq!(UnifiedError::session_not_found("x").category(), "session");
224        assert_eq!(UnifiedError::internal("x").category(), "internal");
225    }
226
227    #[test]
228    fn backpressure_error() {
229        let e = UnifiedError::Backpressure {
230            active: 10,
231            max: 10,
232        };
233        assert!(e.is_retryable());
234        assert!(!e.is_client_error());
235        assert_eq!(e.code(), 2002);
236    }
237
238    #[test]
239    fn crypto_error_codes() {
240        assert_eq!(
241            UnifiedError::Crypto(CryptoErrorKind::InvalidSignature).code(),
242            4001
243        );
244        assert_eq!(
245            UnifiedError::Crypto(CryptoErrorKind::InvalidShare).code(),
246            4002
247        );
248    }
249
250    #[test]
251    fn convenience_constructors() {
252        let e = UnifiedError::unauthorized("bad token");
253        assert!(e.is_client_error());
254        assert_eq!(e.code(), 1006);
255    }
256
257    #[test]
258    fn result_type_alias() {
259        let r: UnifiedResult<i32> = Err(UnifiedError::internal("fail"));
260        assert!(r.is_err());
261    }
262}