orcs_event/error.rs
1//! Event layer errors.
2//!
3//! This module defines error types for the event system,
4//! implementing the [`ErrorCode`] trait for unified error handling
5//! across the ORCS system.
6//!
7//! # Error Code Convention
8//!
9//! All event errors use the `EVENT_` prefix for their codes:
10//!
11//! | Error | Code | Recoverable |
12//! |-------|------|-------------|
13//! | [`EventError::Timeout`] | `EVENT_TIMEOUT` | Yes |
14//! | [`EventError::InvalidSignal`] | `EVENT_INVALID_SIGNAL` | No |
15//! | [`EventError::InvalidRequest`] | `EVENT_INVALID_REQUEST` | No |
16//! | [`EventError::PermissionDenied`] | `EVENT_PERMISSION_DENIED` | No |
17//!
18//! # Recoverability
19//!
20//! An error is **recoverable** if retrying the operation may succeed:
21//!
22//! - **Timeout**: Target may become available (network, busy component)
23//! - **Invalid Signal**: Won't change on retry (bug in caller)
24//! - **Permission Denied**: Requires elevation, not retry
25//!
26//! # Integration with ErrorCode
27//!
28//! All errors implement [`ErrorCode`] for:
29//!
30//! - **Machine-readable codes**: For programmatic error handling
31//! - **Recoverability info**: For retry logic and user feedback
32//! - **Unified logging**: Consistent format across crates
33//!
34//! # Usage
35//!
36//! ```
37//! use orcs_event::EventError;
38//! use orcs_types::{ErrorCode, RequestId};
39//!
40//! fn handle_error(err: EventError) {
41//! // Log with error code
42//! eprintln!("[{}] {}", err.code(), err);
43//!
44//! // Decide on retry
45//! if err.is_recoverable() {
46//! eprintln!("Retrying...");
47//! } else {
48//! eprintln!("Fatal error, cannot retry");
49//! }
50//! }
51//!
52//! let err = EventError::Timeout(RequestId::new());
53//! handle_error(err);
54//! ```
55//!
56//! # Error Handling Patterns
57//!
58//! ## Retry with Backoff
59//!
60//! ```
61//! use orcs_event::EventError;
62//! use orcs_types::{ErrorCode, RequestId};
63//!
64//! fn retry_on_timeout<T, F>(mut operation: F, max_retries: usize) -> Result<T, EventError>
65//! where
66//! F: FnMut() -> Result<T, EventError>,
67//! {
68//! let mut attempts = 0;
69//! loop {
70//! match operation() {
71//! Ok(result) => return Ok(result),
72//! Err(err) if err.is_recoverable() && attempts < max_retries => {
73//! attempts += 1;
74//! // In real code: add exponential backoff
75//! continue;
76//! }
77//! Err(err) => return Err(err),
78//! }
79//! }
80//! }
81//! ```
82//!
83//! ## Error Code Matching
84//!
85//! ```
86//! use orcs_event::EventError;
87//! use orcs_types::{ErrorCode, RequestId};
88//!
89//! fn handle_by_code(err: &EventError) {
90//! match err.code() {
91//! "EVENT_TIMEOUT" => {
92//! // Handle timeout specifically
93//! }
94//! "EVENT_PERMISSION_DENIED" => {
95//! // Prompt for elevation
96//! }
97//! code if code.starts_with("EVENT_") => {
98//! // Generic event error handling
99//! }
100//! _ => unreachable!("Unknown error code"),
101//! }
102//! }
103//! ```
104
105use orcs_types::{ErrorCode, RequestId};
106use serde::{Deserialize, Serialize};
107use thiserror::Error;
108
109/// Event layer error.
110///
111/// All variants implement [`ErrorCode`] for standardized
112/// error handling across the ORCS system.
113///
114/// # Variants
115///
116/// | Variant | When | Recovery |
117/// |---------|------|----------|
118/// | [`Timeout`](Self::Timeout) | Request exceeded timeout | Retry may help |
119/// | [`InvalidSignal`](Self::InvalidSignal) | Malformed signal | Fix the caller |
120/// | [`InvalidRequest`](Self::InvalidRequest) | Malformed request | Fix the caller |
121/// | [`PermissionDenied`](Self::PermissionDenied) | Insufficient privilege | Elevate session |
122///
123/// # Example
124///
125/// ```
126/// use orcs_event::EventError;
127/// use orcs_types::{ErrorCode, RequestId};
128///
129/// let err = EventError::Timeout(RequestId::new());
130/// assert_eq!(err.code(), "EVENT_TIMEOUT");
131/// assert!(err.is_recoverable());
132/// ```
133#[derive(Debug, Clone, Serialize, Deserialize, Error)]
134pub enum EventError {
135 /// Request timed out waiting for response.
136 ///
137 /// This is **recoverable** - retry may succeed if the target
138 /// component becomes available or finishes current work.
139 ///
140 /// # Common Causes
141 ///
142 /// - Target component is busy processing other requests
143 /// - Network latency (in distributed scenarios)
144 /// - Target component has crashed (requires restart)
145 ///
146 /// # Recovery
147 ///
148 /// - Retry with exponential backoff
149 /// - Increase timeout for slow operations
150 /// - Check target component health
151 ///
152 /// # Example
153 ///
154 /// ```
155 /// use orcs_event::EventError;
156 /// use orcs_types::{ErrorCode, RequestId};
157 ///
158 /// let req_id = RequestId::new();
159 /// let err = EventError::Timeout(req_id);
160 ///
161 /// assert!(err.is_recoverable());
162 /// assert!(err.to_string().contains("timed out"));
163 /// ```
164 #[error("request timed out: {0}")]
165 Timeout(RequestId),
166
167 /// Invalid signal was received.
168 ///
169 /// This is **not recoverable** - the signal format or content
170 /// is invalid and won't change on retry.
171 ///
172 /// # Common Causes
173 ///
174 /// - Malformed signal payload
175 /// - Invalid scope specification
176 /// - Deserialization failure
177 ///
178 /// # Recovery
179 ///
180 /// - Fix the signal construction in the caller
181 /// - Validate signal before sending
182 ///
183 /// # Example
184 ///
185 /// ```
186 /// use orcs_event::EventError;
187 /// use orcs_types::ErrorCode;
188 ///
189 /// let err = EventError::InvalidSignal("missing scope".into());
190 ///
191 /// assert!(!err.is_recoverable());
192 /// assert!(err.to_string().contains("invalid signal"));
193 /// ```
194 #[error("invalid signal: {0}")]
195 InvalidSignal(String),
196
197 /// Invalid request was constructed.
198 ///
199 /// This is **not recoverable** - the request format or content
200 /// is invalid and won't change on retry.
201 ///
202 /// # Common Causes
203 ///
204 /// - Empty operation string
205 /// - Invalid payload format
206 ///
207 /// # Recovery
208 ///
209 /// - Fix the request construction in the caller
210 /// - Validate parameters before constructing
211 ///
212 /// # Example
213 ///
214 /// ```
215 /// use orcs_event::EventError;
216 /// use orcs_types::ErrorCode;
217 ///
218 /// let err = EventError::InvalidRequest("operation cannot be empty".into());
219 ///
220 /// assert!(!err.is_recoverable());
221 /// assert!(err.to_string().contains("invalid request"));
222 /// ```
223 #[error("invalid request: {0}")]
224 InvalidRequest(String),
225
226 /// Permission denied for the requested operation.
227 ///
228 /// This is **not recoverable by retry** - requires privilege
229 /// elevation or different credentials.
230 ///
231 /// # Common Causes
232 ///
233 /// - Attempting Global signal without elevated privilege
234 /// - Component trying to signal outside its scope
235 /// - Session has expired elevation
236 ///
237 /// # Recovery
238 ///
239 /// - Elevate the session: `session.elevate(duration)`
240 /// - Use a more limited scope
241 /// - Request elevation from Human
242 ///
243 /// # Permission Model
244 ///
245 /// ```text
246 /// | Scope | Standard | Elevated |
247 /// |-----------------|----------|----------|
248 /// | Global | Denied | Allowed |
249 /// | Channel (own) | Allowed | Allowed |
250 /// | Channel (other) | Denied | Allowed |
251 /// ```
252 ///
253 /// # Example
254 ///
255 /// ```
256 /// use orcs_event::EventError;
257 /// use orcs_types::ErrorCode;
258 ///
259 /// let err = EventError::PermissionDenied(
260 /// "Global scope requires elevated privilege".into()
261 /// );
262 ///
263 /// assert!(!err.is_recoverable());
264 /// assert_eq!(err.code(), "EVENT_PERMISSION_DENIED");
265 /// ```
266 #[error("permission denied: {0}")]
267 PermissionDenied(String),
268}
269
270impl ErrorCode for EventError {
271 /// Returns a machine-readable error code.
272 ///
273 /// All event errors use the `EVENT_` prefix.
274 ///
275 /// # Codes
276 ///
277 /// - `EVENT_TIMEOUT` - Request timed out
278 /// - `EVENT_INVALID_SIGNAL` - Malformed signal
279 /// - `EVENT_INVALID_REQUEST` - Malformed request
280 /// - `EVENT_PERMISSION_DENIED` - Insufficient privilege
281 fn code(&self) -> &'static str {
282 match self {
283 Self::Timeout(_) => "EVENT_TIMEOUT",
284 Self::InvalidSignal(_) => "EVENT_INVALID_SIGNAL",
285 Self::InvalidRequest(_) => "EVENT_INVALID_REQUEST",
286 Self::PermissionDenied(_) => "EVENT_PERMISSION_DENIED",
287 }
288 }
289
290 /// Returns whether the error is recoverable.
291 ///
292 /// # Returns
293 ///
294 /// - `true`: Retry may succeed ([`Timeout`](Self::Timeout))
295 /// - `false`: Retry will not help, requires different action
296 fn is_recoverable(&self) -> bool {
297 match self {
298 Self::Timeout(_) => true,
299 Self::InvalidSignal(_) => false,
300 Self::InvalidRequest(_) => false,
301 Self::PermissionDenied(_) => false,
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use orcs_types::assert_error_codes;
310
311 /// All variants for exhaustive testing
312 fn all_variants() -> Vec<EventError> {
313 vec![
314 EventError::Timeout(RequestId::new()),
315 EventError::InvalidSignal("x".into()),
316 EventError::InvalidRequest("x".into()),
317 EventError::PermissionDenied("x".into()),
318 ]
319 }
320
321 #[test]
322 fn all_error_codes_valid() {
323 // This test ensures ALL variants have correct prefix and format
324 assert_error_codes(&all_variants(), "EVENT_");
325 }
326
327 #[test]
328 fn timeout_error() {
329 let req_id = RequestId::new();
330 let err = EventError::Timeout(req_id);
331
332 assert_eq!(err.code(), "EVENT_TIMEOUT");
333 assert!(err.is_recoverable());
334 assert!(err.to_string().contains("timed out"));
335 }
336
337 #[test]
338 fn invalid_signal_error() {
339 let err = EventError::InvalidSignal("bad format".into());
340
341 assert_eq!(err.code(), "EVENT_INVALID_SIGNAL");
342 assert!(!err.is_recoverable());
343 assert!(err.to_string().contains("invalid signal"));
344 }
345
346 #[test]
347 fn invalid_request_error() {
348 let err = EventError::InvalidRequest("operation cannot be empty".into());
349
350 assert_eq!(err.code(), "EVENT_INVALID_REQUEST");
351 assert!(!err.is_recoverable());
352 assert!(err.to_string().contains("invalid request"));
353 }
354
355 #[test]
356 fn permission_denied_error() {
357 let err = EventError::PermissionDenied("Global signal requires elevation".into());
358
359 assert_eq!(err.code(), "EVENT_PERMISSION_DENIED");
360 assert!(!err.is_recoverable());
361 assert!(err.to_string().contains("permission denied"));
362 }
363}