Skip to main content

hypen_engine/
error.rs

1//! Error types for Hypen Engine operations.
2//!
3//! Provides structured errors that SDK authors can pattern-match on,
4//! replacing the previous `Result<_, String>` API.
5//!
6//! # Example
7//!
8//! ```rust
9//! use hypen_engine::EngineError;
10//!
11//! fn handle_error(err: EngineError) {
12//!     match err {
13//!         EngineError::ParseError { source, message } => {
14//!             eprintln!("Failed to parse '{}': {}", source, message);
15//!         }
16//!         EngineError::ComponentNotFound(name) => {
17//!             eprintln!("Component '{}' not found", name);
18//!         }
19//!         EngineError::ActionNotFound(name) => {
20//!             eprintln!("No handler for action '{}'", name);
21//!         }
22//!         EngineError::RenderError(msg) => {
23//!             eprintln!("Render failed: {}", msg);
24//!         }
25//!         EngineError::StateError(msg) => {
26//!             eprintln!("State error: {}", msg);
27//!         }
28//!         EngineError::ExpressionError(msg) => {
29//!             eprintln!("Expression error: {}", msg);
30//!         }
31//!     }
32//! }
33//! ```
34
35use std::fmt;
36
37/// Structured error type for Hypen Engine operations.
38///
39/// SDK authors can pattern-match on variants to distinguish between
40/// different failure modes and provide appropriate error messages.
41#[derive(Debug, Clone, PartialEq)]
42pub enum EngineError {
43    /// Error parsing Hypen DSL source code.
44    ///
45    /// `source` contains the input text (or a truncated prefix) that failed to parse.
46    /// `message` contains the human-readable parse error.
47    ParseError { source: String, message: String },
48
49    /// A referenced component was not found in the registry.
50    ///
51    /// Contains the component name that could not be resolved.
52    ComponentNotFound(String),
53
54    /// Error during rendering or reconciliation.
55    RenderError(String),
56
57    /// No handler registered for the dispatched action.
58    ///
59    /// Contains the action name that had no handler.
60    ActionNotFound(String),
61
62    /// Error related to state operations (invalid patch, deserialization failure).
63    StateError(String),
64
65    /// Error evaluating an expression or template string.
66    ExpressionError(String),
67}
68
69impl fmt::Display for EngineError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            EngineError::ParseError { source, message } => {
73                write!(f, "Parse error in '{}': {}", truncate(source, 60), message)
74            }
75            EngineError::ComponentNotFound(name) => {
76                write!(f, "Component not found: {}", name)
77            }
78            EngineError::RenderError(msg) => {
79                write!(f, "Render error: {}", msg)
80            }
81            EngineError::ActionNotFound(name) => {
82                write!(f, "No handler registered for action: {}", name)
83            }
84            EngineError::StateError(msg) => {
85                write!(f, "State error: {}", msg)
86            }
87            EngineError::ExpressionError(msg) => {
88                write!(f, "Expression error: {}", msg)
89            }
90        }
91    }
92}
93
94impl std::error::Error for EngineError {}
95
96/// Convert from a String error (backward compatibility for internal code).
97impl From<String> for EngineError {
98    fn from(msg: String) -> Self {
99        EngineError::RenderError(msg)
100    }
101}
102
103/// Truncate a string for display in error messages.
104fn truncate(s: &str, max_len: usize) -> &str {
105    if s.len() <= max_len {
106        s
107    } else {
108        // Find a valid char boundary
109        let mut end = max_len;
110        while !s.is_char_boundary(end) && end > 0 {
111            end -= 1;
112        }
113        &s[..end]
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_parse_error_display() {
123        let err = EngineError::ParseError {
124            source: "Column { broken".to_string(),
125            message: "unexpected end of input".to_string(),
126        };
127        let msg = err.to_string();
128        assert!(msg.contains("Parse error"));
129        assert!(msg.contains("Column { broken"));
130        assert!(msg.contains("unexpected end of input"));
131    }
132
133    #[test]
134    fn test_component_not_found_display() {
135        let err = EngineError::ComponentNotFound("MyWidget".to_string());
136        assert_eq!(err.to_string(), "Component not found: MyWidget");
137    }
138
139    #[test]
140    fn test_action_not_found_display() {
141        let err = EngineError::ActionNotFound("submitForm".to_string());
142        assert_eq!(
143            err.to_string(),
144            "No handler registered for action: submitForm"
145        );
146    }
147
148    #[test]
149    fn test_render_error_display() {
150        let err = EngineError::RenderError("node tree corrupted".to_string());
151        assert_eq!(err.to_string(), "Render error: node tree corrupted");
152    }
153
154    #[test]
155    fn test_state_error_display() {
156        let err = EngineError::StateError("invalid JSON".to_string());
157        assert_eq!(err.to_string(), "State error: invalid JSON");
158    }
159
160    #[test]
161    fn test_expression_error_display() {
162        let err = EngineError::ExpressionError("division by zero".to_string());
163        assert_eq!(err.to_string(), "Expression error: division by zero");
164    }
165
166    #[test]
167    fn test_error_is_clone_and_eq() {
168        let err1 = EngineError::ActionNotFound("test".to_string());
169        let err2 = err1.clone();
170        assert_eq!(err1, err2);
171    }
172
173    #[test]
174    fn test_error_debug() {
175        let err = EngineError::ComponentNotFound("Foo".to_string());
176        let debug = format!("{:?}", err);
177        assert!(debug.contains("ComponentNotFound"));
178        assert!(debug.contains("Foo"));
179    }
180
181    #[test]
182    fn test_from_string() {
183        let err: EngineError = "something went wrong".to_string().into();
184        assert_eq!(
185            err,
186            EngineError::RenderError("something went wrong".to_string())
187        );
188    }
189
190    #[test]
191    fn test_truncate_long_source() {
192        let long_source = "a".repeat(200);
193        let err = EngineError::ParseError {
194            source: long_source,
195            message: "error".to_string(),
196        };
197        let display = err.to_string();
198        // The source should be truncated in the display
199        assert!(display.len() < 200);
200    }
201
202    #[test]
203    fn test_error_implements_std_error() {
204        let err = EngineError::StateError("test".to_string());
205        let std_err: &dyn std::error::Error = &err;
206        assert!(std_err.to_string().contains("State error"));
207    }
208}