Skip to main content

orcs_runtime/engine/
error.rs

1//! Engine Layer Errors.
2//!
3//! This module defines errors for the Engine layer.
4//! All errors implement [`ErrorCode`] for standardized handling.
5//!
6//! # Error Codes
7//!
8//! | Variant | Code | Recoverable |
9//! |---------|------|-------------|
10//! | [`EngineError::ComponentNotFound`] | `ENGINE_COMPONENT_NOT_FOUND` | No |
11//! | [`EngineError::ChannelNotFound`] | `ENGINE_CHANNEL_NOT_FOUND` | No |
12//! | [`EngineError::NoTarget`] | `ENGINE_NO_TARGET` | No |
13//! | [`EngineError::SendFailed`] | `ENGINE_SEND_FAILED` | Yes |
14//! | [`EngineError::ChannelClosed`] | `ENGINE_CHANNEL_CLOSED` | No |
15//! | [`EngineError::NotRunning`] | `ENGINE_NOT_RUNNING` | No |
16//! | [`EngineError::Timeout`] | `ENGINE_TIMEOUT` | Yes |
17//! | [`EngineError::ComponentFailed`] | `ENGINE_COMPONENT_FAILED` | No |
18//! | [`EngineError::NoSubscriber`] | `ENGINE_NO_SUBSCRIBER` | No |
19//!
20//! # Recoverability
21//!
22//! Recoverable errors may succeed on retry:
23//! - `SendFailed`: Transient channel issue
24//! - `Timeout`: Slow response, may complete later
25//!
26//! Non-recoverable errors require code/config changes.
27
28use orcs_event::EventCategory;
29use orcs_types::{ChannelId, ComponentId, ErrorCode, RequestId};
30use thiserror::Error;
31
32/// Engine layer error.
33///
34/// Represents failures within the Engine runtime and EventBus.
35/// Implements [`ErrorCode`] for standardized error handling.
36///
37/// # Example
38///
39/// ```
40/// use orcs_runtime::EngineError;
41/// use orcs_types::ErrorCode;
42///
43/// let err = EngineError::NoTarget;
44/// assert_eq!(err.code(), "ENGINE_NO_TARGET");
45/// assert!(!err.is_recoverable());
46/// ```
47#[derive(Debug, Clone, Error)]
48pub enum EngineError {
49    /// Component not found in EventBus registry.
50    #[error("component not found: {0}")]
51    ComponentNotFound(ComponentId),
52
53    /// Channel not found for event injection.
54    #[error("channel not found: {0}")]
55    ChannelNotFound(ChannelId),
56
57    /// Request target is required but not specified.
58    #[error("request target is required")]
59    NoTarget,
60
61    /// Failed to send message to component.
62    #[error("send failed: {0}")]
63    SendFailed(String),
64
65    /// Component channel was closed unexpectedly.
66    #[error("channel closed")]
67    ChannelClosed,
68
69    /// Engine is not running.
70    #[error("engine not running")]
71    NotRunning,
72
73    /// Request timed out waiting for response.
74    #[error("request timed out: {0}")]
75    Timeout(RequestId),
76
77    /// Component returned an error.
78    #[error("component error: {0}")]
79    ComponentFailed(String),
80
81    /// No subscriber for the requested category.
82    #[error("no subscriber for category: {0}")]
83    NoSubscriber(EventCategory),
84
85    /// Package operation failed.
86    #[error("package error: {0}")]
87    PackageFailed(String),
88
89    /// Component does not support packages.
90    #[error("component does not support packages: {0}")]
91    PackageNotSupported(ComponentId),
92}
93
94impl ErrorCode for EngineError {
95    fn code(&self) -> &'static str {
96        match self {
97            Self::ComponentNotFound(_) => "ENGINE_COMPONENT_NOT_FOUND",
98            Self::ChannelNotFound(_) => "ENGINE_CHANNEL_NOT_FOUND",
99            Self::NoTarget => "ENGINE_NO_TARGET",
100            Self::SendFailed(_) => "ENGINE_SEND_FAILED",
101            Self::ChannelClosed => "ENGINE_CHANNEL_CLOSED",
102            Self::NotRunning => "ENGINE_NOT_RUNNING",
103            Self::Timeout(_) => "ENGINE_TIMEOUT",
104            Self::ComponentFailed(_) => "ENGINE_COMPONENT_FAILED",
105            Self::NoSubscriber(_) => "ENGINE_NO_SUBSCRIBER",
106            Self::PackageFailed(_) => "ENGINE_PACKAGE_FAILED",
107            Self::PackageNotSupported(_) => "ENGINE_PACKAGE_NOT_SUPPORTED",
108        }
109    }
110
111    fn is_recoverable(&self) -> bool {
112        matches!(self, Self::SendFailed(_) | Self::Timeout(_))
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use orcs_types::{assert_error_codes, ChannelId};
120
121    fn all_variants() -> Vec<EngineError> {
122        vec![
123            EngineError::ComponentNotFound(ComponentId::builtin("x")),
124            EngineError::ChannelNotFound(ChannelId::new()),
125            EngineError::NoTarget,
126            EngineError::SendFailed("x".into()),
127            EngineError::ChannelClosed,
128            EngineError::NotRunning,
129            EngineError::Timeout(RequestId::new()),
130            EngineError::ComponentFailed("x".into()),
131            EngineError::NoSubscriber(EventCategory::Echo),
132            EngineError::PackageFailed("x".into()),
133            EngineError::PackageNotSupported(ComponentId::builtin("x")),
134        ]
135    }
136
137    #[test]
138    fn all_error_codes_valid() {
139        assert_error_codes(&all_variants(), "ENGINE_");
140    }
141
142    #[test]
143    fn engine_error_codes() {
144        let err = EngineError::NoTarget;
145        assert_eq!(err.code(), "ENGINE_NO_TARGET");
146
147        let err = EngineError::ComponentNotFound(ComponentId::builtin("x"));
148        assert_eq!(err.code(), "ENGINE_COMPONENT_NOT_FOUND");
149
150        let err = EngineError::Timeout(RequestId::new());
151        assert_eq!(err.code(), "ENGINE_TIMEOUT");
152
153        let err = EngineError::ComponentFailed("test".into());
154        assert_eq!(err.code(), "ENGINE_COMPONENT_FAILED");
155    }
156
157    #[test]
158    fn engine_error_recoverable() {
159        assert!(!EngineError::NoTarget.is_recoverable());
160        assert!(EngineError::SendFailed("x".into()).is_recoverable());
161        assert!(EngineError::Timeout(RequestId::new()).is_recoverable());
162        assert!(!EngineError::ComponentFailed("x".into()).is_recoverable());
163    }
164}