Skip to main content

jpx_engine/
error.rs

1//! Error types for the jpx engine.
2//!
3//! This module defines the error types used throughout the engine.
4//! All public methods that can fail return [`Result<T>`](Result).
5//!
6//! # Error Handling
7//!
8//! ```rust
9//! use jpx_engine::{JpxEngine, EngineError, EvaluationErrorKind};
10//!
11//! let engine = JpxEngine::new();
12//!
13//! // Handle specific error types
14//! match engine.evaluate("invalid[", &serde_json::json!({})) {
15//!     Ok(result) => println!("Result: {}", result),
16//!     Err(EngineError::InvalidExpression(msg)) => {
17//!         eprintln!("Syntax error: {}", msg);
18//!     }
19//!     Err(e) => eprintln!("Other error: {}", e),
20//! }
21//! ```
22//!
23//! # Structured Evaluation Errors
24//!
25//! [`EvaluationFailed`](EngineError::EvaluationFailed) carries an
26//! [`EvaluationErrorKind`] that lets consumers match on specific failure modes
27//! without parsing error strings:
28//!
29//! ```rust
30//! use jpx_engine::{JpxEngine, EngineError, EvaluationErrorKind};
31//!
32//! let engine = JpxEngine::strict();
33//!
34//! match engine.evaluate("sum(@)", &serde_json::json!({})) {
35//!     Err(EngineError::EvaluationFailed { kind: EvaluationErrorKind::UndefinedFunction { ref name }, .. }) => {
36//!         println!("Unknown function: {}", name);
37//!     }
38//!     _ => {}
39//! }
40//! ```
41
42use std::fmt;
43use thiserror::Error;
44
45/// Classifies the specific failure mode of an evaluation error.
46///
47/// This allows consumers to programmatically handle different error types
48/// without parsing error message strings.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum EvaluationErrorKind {
51    /// Expression called a function that is not defined.
52    ///
53    /// Common when using extension functions in strict mode, or typos
54    /// in function names.
55    UndefinedFunction {
56        /// The function name that was not found.
57        name: String,
58    },
59    /// Wrong number of arguments passed to a function.
60    ArgumentCount {
61        /// Expected number of arguments (if parseable).
62        expected: Option<u32>,
63        /// Actual number of arguments (if parseable).
64        actual: Option<u32>,
65    },
66    /// Argument type does not match what the function expects.
67    TypeError {
68        /// Description of the type mismatch.
69        detail: String,
70    },
71    /// Any other evaluation failure.
72    Other,
73}
74
75impl fmt::Display for EvaluationErrorKind {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            EvaluationErrorKind::UndefinedFunction { name } => {
79                write!(f, "undefined function '{}'", name)
80            }
81            EvaluationErrorKind::ArgumentCount {
82                expected: Some(exp),
83                actual: Some(act),
84            } => write!(f, "expected {} arguments, found {}", exp, act),
85            EvaluationErrorKind::ArgumentCount { .. } => write!(f, "wrong number of arguments"),
86            EvaluationErrorKind::TypeError { detail } => write!(f, "type error: {}", detail),
87            EvaluationErrorKind::Other => write!(f, "evaluation error"),
88        }
89    }
90}
91
92/// Parse a jpx-core runtime error message into a structured kind.
93///
94/// The jpx-core runtime produces predictable error message patterns:
95/// - `"Call to undefined function <name>"` for unknown functions
96/// - `"Too many arguments: expected <n>, found <m>"` for argument count errors
97/// - `"Not enough arguments: expected <n>, found <m>"` for argument count errors
98/// - `"Argument <n> expects type <expected>, given <actual>"` for type errors
99pub(crate) fn classify_evaluation_error(message: &str) -> EvaluationErrorKind {
100    // "Call to undefined function <name>"
101    if let Some(rest) = message
102        .strip_prefix("Runtime error: Call to undefined function ")
103        .or_else(|| message.strip_prefix("Call to undefined function "))
104    {
105        // The name is the next word (up to space or parens or end)
106        let name = rest.split([' ', '(']).next().unwrap_or(rest).to_string();
107        return EvaluationErrorKind::UndefinedFunction { name };
108    }
109
110    // "Too many arguments: expected <n>, found <m>"
111    // "Not enough arguments: expected <n>, found <m>"
112    if message.contains("arguments: expected") {
113        let expected = extract_number_after(message, "expected ");
114        let actual = extract_number_after(message, "found ");
115        return EvaluationErrorKind::ArgumentCount { expected, actual };
116    }
117
118    // "Argument <n> expects type <expected>, given <actual>"
119    if message.contains("expects type") {
120        let detail = message
121            .strip_prefix("Runtime error: ")
122            .unwrap_or(message)
123            .to_string();
124        return EvaluationErrorKind::TypeError { detail };
125    }
126
127    EvaluationErrorKind::Other
128}
129
130/// Extract the first number after a prefix string.
131fn extract_number_after(s: &str, prefix: &str) -> Option<u32> {
132    let idx = s.find(prefix)?;
133    let after = &s[idx + prefix.len()..];
134    let num_str: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
135    num_str.parse().ok()
136}
137
138/// Errors that can occur during engine operations.
139///
140/// Each variant represents a specific failure mode, making it easy to
141/// handle different error types appropriately.
142#[derive(Debug, Error)]
143pub enum EngineError {
144    /// JMESPath expression has invalid syntax.
145    ///
146    /// Returned when [`JpxEngine::evaluate`](crate::JpxEngine::evaluate) or
147    /// [`JpxEngine::validate`](crate::JpxEngine::validate) encounters a
148    /// malformed expression.
149    #[error("Invalid expression: {0}")]
150    InvalidExpression(String),
151
152    /// JSON input could not be parsed.
153    ///
154    /// Returned when [`JpxEngine::evaluate_str`](crate::JpxEngine::evaluate_str)
155    /// or similar methods receive invalid JSON.
156    #[error("Invalid JSON: {0}")]
157    InvalidJson(String),
158
159    /// Expression evaluation failed at runtime.
160    ///
161    /// Carries a [`kind`](EvaluationErrorKind) field for programmatic matching
162    /// on specific failure modes (undefined function, argument errors, type errors).
163    #[error("Evaluation failed: {message}")]
164    EvaluationFailed {
165        /// Human-readable error message.
166        message: String,
167        /// Structured classification of the error.
168        kind: EvaluationErrorKind,
169    },
170
171    /// Requested function does not exist.
172    ///
173    /// Returned by introspection methods when a function name is not found.
174    #[error("Unknown function: {0}")]
175    UnknownFunction(String),
176
177    /// Requested stored query does not exist.
178    ///
179    /// Returned by [`JpxEngine::run_query`](crate::JpxEngine::run_query)
180    /// when the named query hasn't been defined.
181    #[error("Query not found: {0}")]
182    QueryNotFound(String),
183
184    /// Discovery registration failed.
185    ///
186    /// Returned when registering a discovery spec fails validation
187    /// or conflicts with an existing registration.
188    #[error("Registration failed: {0}")]
189    RegistrationFailed(String),
190
191    /// Configuration error (parse failure, invalid settings, etc.).
192    ///
193    /// Returned when loading or merging configuration files fails.
194    #[error("Config error: {0}")]
195    ConfigError(String),
196
197    /// Internal error (lock poisoning, serialization failure, etc.).
198    ///
199    /// These errors indicate bugs or unexpected conditions and should
200    /// generally be reported.
201    #[error("Internal error: {0}")]
202    Internal(String),
203
204    /// Arrow conversion error (only available with `arrow` feature).
205    ///
206    /// Returned when converting between Arrow RecordBatches and JSON fails.
207    #[cfg(feature = "arrow")]
208    #[error("Arrow error: {0}")]
209    ArrowError(String),
210}
211
212impl EngineError {
213    /// Create an `EvaluationFailed` error, automatically classifying the error kind
214    /// from the message string.
215    pub(crate) fn evaluation_failed(message: String) -> Self {
216        let kind = classify_evaluation_error(&message);
217        EngineError::EvaluationFailed { message, kind }
218    }
219}
220
221/// A specialized Result type for engine operations.
222///
223/// This is defined as `std::result::Result<T, EngineError>` for convenience.
224pub type Result<T> = std::result::Result<T, EngineError>;
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_classify_undefined_function() {
232        let kind = classify_evaluation_error(
233            "Runtime error: Call to undefined function foo_bar (line 0, column 7)",
234        );
235        assert_eq!(
236            kind,
237            EvaluationErrorKind::UndefinedFunction {
238                name: "foo_bar".to_string()
239            }
240        );
241    }
242
243    #[test]
244    fn test_classify_too_many_arguments() {
245        let kind = classify_evaluation_error(
246            "Runtime error: Too many arguments: expected 1, found 2 (line 0, column 6)",
247        );
248        assert_eq!(
249            kind,
250            EvaluationErrorKind::ArgumentCount {
251                expected: Some(1),
252                actual: Some(2)
253            }
254        );
255    }
256
257    #[test]
258    fn test_classify_not_enough_arguments() {
259        let kind = classify_evaluation_error(
260            "Runtime error: Not enough arguments: expected 2, found 1 (line 0, column 4)",
261        );
262        assert_eq!(
263            kind,
264            EvaluationErrorKind::ArgumentCount {
265                expected: Some(2),
266                actual: Some(1)
267            }
268        );
269    }
270
271    #[test]
272    fn test_classify_type_error() {
273        let kind = classify_evaluation_error(
274            "Runtime error: Argument 0 expects type array[number], given object (line 0, column 3)",
275        );
276        match kind {
277            EvaluationErrorKind::TypeError { detail } => {
278                assert!(detail.contains("expects type"));
279            }
280            other => panic!("Expected TypeError, got {:?}", other),
281        }
282    }
283
284    #[test]
285    fn test_classify_other() {
286        let kind = classify_evaluation_error("Some unknown error");
287        assert_eq!(kind, EvaluationErrorKind::Other);
288    }
289
290    // Display formatting tests for EvaluationErrorKind
291
292    #[test]
293    fn test_display_undefined_function() {
294        let kind = EvaluationErrorKind::UndefinedFunction {
295            name: "foo".to_string(),
296        };
297        assert_eq!(kind.to_string(), "undefined function 'foo'");
298    }
299
300    #[test]
301    fn test_display_argument_count_with_numbers() {
302        let kind = EvaluationErrorKind::ArgumentCount {
303            expected: Some(2),
304            actual: Some(3),
305        };
306        assert_eq!(kind.to_string(), "expected 2 arguments, found 3");
307    }
308
309    #[test]
310    fn test_display_argument_count_without_numbers() {
311        let kind = EvaluationErrorKind::ArgumentCount {
312            expected: None,
313            actual: None,
314        };
315        assert_eq!(kind.to_string(), "wrong number of arguments");
316    }
317
318    #[test]
319    fn test_display_type_error() {
320        let kind = EvaluationErrorKind::TypeError {
321            detail: "some detail".to_string(),
322        };
323        assert_eq!(kind.to_string(), "type error: some detail");
324    }
325
326    #[test]
327    fn test_display_other() {
328        let kind = EvaluationErrorKind::Other;
329        assert_eq!(kind.to_string(), "evaluation error");
330    }
331
332    // EngineError Display tests
333
334    #[test]
335    fn test_engine_error_display_invalid_expression() {
336        let err = EngineError::InvalidExpression("unexpected token".to_string());
337        assert_eq!(err.to_string(), "Invalid expression: unexpected token");
338    }
339
340    #[test]
341    fn test_engine_error_display_invalid_json() {
342        let err = EngineError::InvalidJson("expected value at line 1".to_string());
343        assert_eq!(err.to_string(), "Invalid JSON: expected value at line 1");
344    }
345
346    #[test]
347    fn test_engine_error_display_evaluation_failed() {
348        let err = EngineError::EvaluationFailed {
349            message: "something went wrong".to_string(),
350            kind: EvaluationErrorKind::Other,
351        };
352        assert_eq!(err.to_string(), "Evaluation failed: something went wrong");
353    }
354
355    #[test]
356    fn test_engine_error_display_all_variants() {
357        let unknown = EngineError::UnknownFunction("mystery".to_string());
358        assert_eq!(unknown.to_string(), "Unknown function: mystery");
359
360        let not_found = EngineError::QueryNotFound("my_query".to_string());
361        assert_eq!(not_found.to_string(), "Query not found: my_query");
362
363        let reg_failed = EngineError::RegistrationFailed("duplicate name".to_string());
364        assert_eq!(
365            reg_failed.to_string(),
366            "Registration failed: duplicate name"
367        );
368
369        let config = EngineError::ConfigError("missing field".to_string());
370        assert_eq!(config.to_string(), "Config error: missing field");
371
372        let internal = EngineError::Internal("lock poisoned".to_string());
373        assert_eq!(internal.to_string(), "Internal error: lock poisoned");
374    }
375
376    // Constructor test
377
378    #[test]
379    fn test_evaluation_failed_constructor() {
380        let err = EngineError::evaluation_failed("Call to undefined function foo".to_string());
381        match err {
382            EngineError::EvaluationFailed {
383                ref message,
384                ref kind,
385            } => {
386                assert_eq!(message, "Call to undefined function foo");
387                assert_eq!(
388                    *kind,
389                    EvaluationErrorKind::UndefinedFunction {
390                        name: "foo".to_string()
391                    }
392                );
393            }
394            other => panic!("Expected EvaluationFailed, got {:?}", other),
395        }
396    }
397}