1use std::fmt;
4
5#[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
100impl 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
167impl 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}