jpx-engine 0.4.0

JMESPath query engine with introspection, discovery, and advanced features
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
//! Error types for the jpx engine.
//!
//! This module defines the error types used throughout the engine.
//! All public methods that can fail return [`Result<T>`](Result).
//!
//! # Error Handling
//!
//! ```rust
//! use jpx_engine::{JpxEngine, EngineError, EvaluationErrorKind};
//!
//! let engine = JpxEngine::new();
//!
//! // Handle specific error types
//! match engine.evaluate("invalid[", &serde_json::json!({})) {
//!     Ok(result) => println!("Result: {}", result),
//!     Err(EngineError::InvalidExpression(msg)) => {
//!         eprintln!("Syntax error: {}", msg);
//!     }
//!     Err(e) => eprintln!("Other error: {}", e),
//! }
//! ```
//!
//! # Structured Evaluation Errors
//!
//! [`EvaluationFailed`](EngineError::EvaluationFailed) carries an
//! [`EvaluationErrorKind`] that lets consumers match on specific failure modes
//! without parsing error strings:
//!
//! ```rust
//! use jpx_engine::{JpxEngine, EngineError, EvaluationErrorKind};
//!
//! let engine = JpxEngine::strict();
//!
//! match engine.evaluate("sum(@)", &serde_json::json!({})) {
//!     Err(EngineError::EvaluationFailed { kind: EvaluationErrorKind::UndefinedFunction { ref name }, .. }) => {
//!         println!("Unknown function: {}", name);
//!     }
//!     _ => {}
//! }
//! ```

use std::fmt;
use thiserror::Error;

/// Classifies the specific failure mode of an evaluation error.
///
/// This allows consumers to programmatically handle different error types
/// without parsing error message strings.
///
/// Marked `#[non_exhaustive]`: new failure modes may be added in future
/// releases, so downstream matches should include a wildcard arm.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EvaluationErrorKind {
    /// Expression called a function that is not defined.
    ///
    /// Common when using extension functions in strict mode, or typos
    /// in function names.
    UndefinedFunction {
        /// The function name that was not found.
        name: String,
    },
    /// Wrong number of arguments passed to a function.
    ArgumentCount {
        /// Expected number of arguments (if parseable).
        expected: Option<u32>,
        /// Actual number of arguments (if parseable).
        actual: Option<u32>,
    },
    /// Argument type does not match what the function expects.
    TypeError {
        /// Description of the type mismatch.
        detail: String,
    },
    /// Any other evaluation failure.
    Other,
}

impl fmt::Display for EvaluationErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            EvaluationErrorKind::UndefinedFunction { name } => {
                write!(f, "undefined function '{}'", name)
            }
            EvaluationErrorKind::ArgumentCount {
                expected: Some(exp),
                actual: Some(act),
            } => write!(f, "expected {} arguments, found {}", exp, act),
            EvaluationErrorKind::ArgumentCount { .. } => write!(f, "wrong number of arguments"),
            EvaluationErrorKind::TypeError { detail } => write!(f, "type error: {}", detail),
            EvaluationErrorKind::Other => write!(f, "evaluation error"),
        }
    }
}

/// Parse a jpx-core runtime error message into a structured kind.
///
/// The jpx-core runtime produces predictable error message patterns:
/// - `"Call to undefined function <name>"` for unknown functions
/// - `"Too many arguments: expected <n>, found <m>"` for argument count errors
/// - `"Not enough arguments: expected <n>, found <m>"` for argument count errors
/// - `"Argument <n> expects type <expected>, given <actual>"` for type errors
pub(crate) fn classify_evaluation_error(message: &str) -> EvaluationErrorKind {
    // "Call to undefined function <name>"
    if let Some(rest) = message
        .strip_prefix("Runtime error: Call to undefined function ")
        .or_else(|| message.strip_prefix("Call to undefined function "))
    {
        // The name is the next word (up to space or parens or end)
        let name = rest.split([' ', '(']).next().unwrap_or(rest).to_string();
        return EvaluationErrorKind::UndefinedFunction { name };
    }

    // "Too many arguments: expected <n>, found <m>"
    // "Not enough arguments: expected <n>, found <m>"
    if message.contains("arguments: expected") {
        let expected = extract_number_after(message, "expected ");
        let actual = extract_number_after(message, "found ");
        return EvaluationErrorKind::ArgumentCount { expected, actual };
    }

    // "Argument <n> expects type <expected>, given <actual>"
    if message.contains("expects type") {
        let detail = message
            .strip_prefix("Runtime error: ")
            .unwrap_or(message)
            .to_string();
        return EvaluationErrorKind::TypeError { detail };
    }

    EvaluationErrorKind::Other
}

/// Extract the first number after a prefix string.
fn extract_number_after(s: &str, prefix: &str) -> Option<u32> {
    let idx = s.find(prefix)?;
    let after = &s[idx + prefix.len()..];
    let num_str: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
    num_str.parse().ok()
}

/// Errors that can occur during engine operations.
///
/// Each variant represents a specific failure mode, making it easy to
/// handle different error types appropriately.
///
/// Marked `#[non_exhaustive]`: new failure modes may be added in future
/// releases, so downstream matches should include a wildcard arm.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum EngineError {
    /// JMESPath expression has invalid syntax.
    ///
    /// Returned when [`JpxEngine::evaluate`](crate::JpxEngine::evaluate) or
    /// [`JpxEngine::validate`](crate::JpxEngine::validate) encounters a
    /// malformed expression.
    #[error("Invalid expression: {0}")]
    InvalidExpression(String),

    /// JSON input could not be parsed.
    ///
    /// Returned when [`JpxEngine::evaluate_str`](crate::JpxEngine::evaluate_str)
    /// or similar methods receive invalid JSON.
    #[error("Invalid JSON: {0}")]
    InvalidJson(String),

    /// Expression evaluation failed at runtime.
    ///
    /// Carries a [`kind`](EvaluationErrorKind) field for programmatic matching
    /// on specific failure modes (undefined function, argument errors, type errors).
    #[error("Evaluation failed: {message}")]
    EvaluationFailed {
        /// Human-readable error message.
        message: String,
        /// Structured classification of the error.
        kind: EvaluationErrorKind,
    },

    /// Requested function does not exist.
    ///
    /// Returned by introspection methods when a function name is not found.
    #[error("Unknown function: {0}")]
    UnknownFunction(String),

    /// Requested stored query does not exist.
    ///
    /// Returned by [`JpxEngine::run_query`](crate::JpxEngine::run_query)
    /// when the named query hasn't been defined.
    #[error("Query not found: {0}")]
    QueryNotFound(String),

    /// Discovery registration failed.
    ///
    /// Returned when registering a discovery spec fails validation
    /// or conflicts with an existing registration.
    #[error("Registration failed: {0}")]
    RegistrationFailed(String),

    /// Configuration error (parse failure, invalid settings, etc.).
    ///
    /// Returned when loading or merging configuration files fails.
    #[error("Config error: {0}")]
    ConfigError(String),

    /// Internal error (lock poisoning, serialization failure, etc.).
    ///
    /// These errors indicate bugs or unexpected conditions and should
    /// generally be reported.
    #[error("Internal error: {0}")]
    Internal(String),

    /// Arrow conversion error (only available with `arrow` feature).
    ///
    /// Returned when converting between Arrow RecordBatches and JSON fails.
    #[cfg(feature = "arrow")]
    #[error("Arrow error: {0}")]
    ArrowError(String),
}

impl EngineError {
    /// Create an `EvaluationFailed` error, automatically classifying the error kind
    /// from the message string.
    pub(crate) fn evaluation_failed(message: String) -> Self {
        let kind = classify_evaluation_error(&message);
        EngineError::EvaluationFailed { message, kind }
    }
}

/// A specialized Result type for engine operations.
///
/// This is defined as `std::result::Result<T, EngineError>` for convenience.
pub type Result<T> = std::result::Result<T, EngineError>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_classify_undefined_function() {
        let kind = classify_evaluation_error(
            "Runtime error: Call to undefined function foo_bar (line 0, column 7)",
        );
        assert_eq!(
            kind,
            EvaluationErrorKind::UndefinedFunction {
                name: "foo_bar".to_string()
            }
        );
    }

    #[test]
    fn test_classify_too_many_arguments() {
        let kind = classify_evaluation_error(
            "Runtime error: Too many arguments: expected 1, found 2 (line 0, column 6)",
        );
        assert_eq!(
            kind,
            EvaluationErrorKind::ArgumentCount {
                expected: Some(1),
                actual: Some(2)
            }
        );
    }

    #[test]
    fn test_classify_not_enough_arguments() {
        let kind = classify_evaluation_error(
            "Runtime error: Not enough arguments: expected 2, found 1 (line 0, column 4)",
        );
        assert_eq!(
            kind,
            EvaluationErrorKind::ArgumentCount {
                expected: Some(2),
                actual: Some(1)
            }
        );
    }

    #[test]
    fn test_classify_type_error() {
        let kind = classify_evaluation_error(
            "Runtime error: Argument 0 expects type array[number], given object (line 0, column 3)",
        );
        match kind {
            EvaluationErrorKind::TypeError { detail } => {
                assert!(detail.contains("expects type"));
            }
            other => panic!("Expected TypeError, got {:?}", other),
        }
    }

    #[test]
    fn test_classify_other() {
        let kind = classify_evaluation_error("Some unknown error");
        assert_eq!(kind, EvaluationErrorKind::Other);
    }

    // Display formatting tests for EvaluationErrorKind

    #[test]
    fn test_display_undefined_function() {
        let kind = EvaluationErrorKind::UndefinedFunction {
            name: "foo".to_string(),
        };
        assert_eq!(kind.to_string(), "undefined function 'foo'");
    }

    #[test]
    fn test_display_argument_count_with_numbers() {
        let kind = EvaluationErrorKind::ArgumentCount {
            expected: Some(2),
            actual: Some(3),
        };
        assert_eq!(kind.to_string(), "expected 2 arguments, found 3");
    }

    #[test]
    fn test_display_argument_count_without_numbers() {
        let kind = EvaluationErrorKind::ArgumentCount {
            expected: None,
            actual: None,
        };
        assert_eq!(kind.to_string(), "wrong number of arguments");
    }

    #[test]
    fn test_display_type_error() {
        let kind = EvaluationErrorKind::TypeError {
            detail: "some detail".to_string(),
        };
        assert_eq!(kind.to_string(), "type error: some detail");
    }

    #[test]
    fn test_display_other() {
        let kind = EvaluationErrorKind::Other;
        assert_eq!(kind.to_string(), "evaluation error");
    }

    // EngineError Display tests

    #[test]
    fn test_engine_error_display_invalid_expression() {
        let err = EngineError::InvalidExpression("unexpected token".to_string());
        assert_eq!(err.to_string(), "Invalid expression: unexpected token");
    }

    #[test]
    fn test_engine_error_display_invalid_json() {
        let err = EngineError::InvalidJson("expected value at line 1".to_string());
        assert_eq!(err.to_string(), "Invalid JSON: expected value at line 1");
    }

    #[test]
    fn test_engine_error_display_evaluation_failed() {
        let err = EngineError::EvaluationFailed {
            message: "something went wrong".to_string(),
            kind: EvaluationErrorKind::Other,
        };
        assert_eq!(err.to_string(), "Evaluation failed: something went wrong");
    }

    #[test]
    fn test_engine_error_display_all_variants() {
        let unknown = EngineError::UnknownFunction("mystery".to_string());
        assert_eq!(unknown.to_string(), "Unknown function: mystery");

        let not_found = EngineError::QueryNotFound("my_query".to_string());
        assert_eq!(not_found.to_string(), "Query not found: my_query");

        let reg_failed = EngineError::RegistrationFailed("duplicate name".to_string());
        assert_eq!(
            reg_failed.to_string(),
            "Registration failed: duplicate name"
        );

        let config = EngineError::ConfigError("missing field".to_string());
        assert_eq!(config.to_string(), "Config error: missing field");

        let internal = EngineError::Internal("lock poisoned".to_string());
        assert_eq!(internal.to_string(), "Internal error: lock poisoned");
    }

    // Constructor test

    #[test]
    fn test_evaluation_failed_constructor() {
        let err = EngineError::evaluation_failed("Call to undefined function foo".to_string());
        match err {
            EngineError::EvaluationFailed {
                ref message,
                ref kind,
            } => {
                assert_eq!(message, "Call to undefined function foo");
                assert_eq!(
                    *kind,
                    EvaluationErrorKind::UndefinedFunction {
                        name: "foo".to_string()
                    }
                );
            }
            other => panic!("Expected EvaluationFailed, got {:?}", other),
        }
    }
}