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