Skip to main content

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}