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 {
48        source: String,
49        message: String,
50    },
51
52    /// A referenced component was not found in the registry.
53    ///
54    /// Contains the component name that could not be resolved.
55    ComponentNotFound(String),
56
57    /// Error during rendering or reconciliation.
58    RenderError(String),
59
60    /// No handler registered for the dispatched action.
61    ///
62    /// Contains the action name that had no handler.
63    ActionNotFound(String),
64
65    /// Error related to state operations (invalid patch, deserialization failure).
66    StateError(String),
67
68    /// Error evaluating an expression or template string.
69    ExpressionError(String),
70}
71
72impl fmt::Display for EngineError {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            EngineError::ParseError { source, message } => {
76                write!(f, "Parse error in '{}': {}", truncate(source, 60), message)
77            }
78            EngineError::ComponentNotFound(name) => {
79                write!(f, "Component not found: {}", name)
80            }
81            EngineError::RenderError(msg) => {
82                write!(f, "Render error: {}", msg)
83            }
84            EngineError::ActionNotFound(name) => {
85                write!(f, "No handler registered for action: {}", name)
86            }
87            EngineError::StateError(msg) => {
88                write!(f, "State error: {}", msg)
89            }
90            EngineError::ExpressionError(msg) => {
91                write!(f, "Expression error: {}", msg)
92            }
93        }
94    }
95}
96
97impl std::error::Error for EngineError {}
98
99/// Convert from a String error (backward compatibility for internal code).
100impl From<String> for EngineError {
101    fn from(msg: String) -> Self {
102        EngineError::RenderError(msg)
103    }
104}
105
106/// Truncate a string for display in error messages.
107fn truncate(s: &str, max_len: usize) -> &str {
108    if s.len() <= max_len {
109        s
110    } else {
111        // Find a valid char boundary
112        let mut end = max_len;
113        while !s.is_char_boundary(end) && end > 0 {
114            end -= 1;
115        }
116        &s[..end]
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_parse_error_display() {
126        let err = EngineError::ParseError {
127            source: "Column { broken".to_string(),
128            message: "unexpected end of input".to_string(),
129        };
130        let msg = err.to_string();
131        assert!(msg.contains("Parse error"));
132        assert!(msg.contains("Column { broken"));
133        assert!(msg.contains("unexpected end of input"));
134    }
135
136    #[test]
137    fn test_component_not_found_display() {
138        let err = EngineError::ComponentNotFound("MyWidget".to_string());
139        assert_eq!(err.to_string(), "Component not found: MyWidget");
140    }
141
142    #[test]
143    fn test_action_not_found_display() {
144        let err = EngineError::ActionNotFound("submitForm".to_string());
145        assert_eq!(
146            err.to_string(),
147            "No handler registered for action: submitForm"
148        );
149    }
150
151    #[test]
152    fn test_render_error_display() {
153        let err = EngineError::RenderError("node tree corrupted".to_string());
154        assert_eq!(err.to_string(), "Render error: node tree corrupted");
155    }
156
157    #[test]
158    fn test_state_error_display() {
159        let err = EngineError::StateError("invalid JSON".to_string());
160        assert_eq!(err.to_string(), "State error: invalid JSON");
161    }
162
163    #[test]
164    fn test_expression_error_display() {
165        let err = EngineError::ExpressionError("division by zero".to_string());
166        assert_eq!(err.to_string(), "Expression error: division by zero");
167    }
168
169    #[test]
170    fn test_error_is_clone_and_eq() {
171        let err1 = EngineError::ActionNotFound("test".to_string());
172        let err2 = err1.clone();
173        assert_eq!(err1, err2);
174    }
175
176    #[test]
177    fn test_error_debug() {
178        let err = EngineError::ComponentNotFound("Foo".to_string());
179        let debug = format!("{:?}", err);
180        assert!(debug.contains("ComponentNotFound"));
181        assert!(debug.contains("Foo"));
182    }
183
184    #[test]
185    fn test_from_string() {
186        let err: EngineError = "something went wrong".to_string().into();
187        assert_eq!(err, EngineError::RenderError("something went wrong".to_string()));
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}