Skip to main content

jsonata_core/
lib.rs

1// jsonatapy - High-performance Python implementation of JSONata
2// Copyright (c) 2025 jsonatapy contributors
3// Licensed under the MIT License
4
5//! # jsonatapy
6//!
7//! A high-performance Rust implementation of JSONata - the JSON query and
8//! transformation language - with optional Python bindings via PyO3.
9//!
10//! ## Rust API
11//!
12//! ```rust,ignore
13//! use jsonata_core::parser;
14//! use jsonata_core::evaluator::Evaluator;
15//! use jsonata_core::value::JValue;
16//!
17//! let ast = parser::parse("user.name").unwrap();
18//! let data = JValue::from_json_str(r#"{"user":{"name":"Alice"}}"#).unwrap();
19//! let result = Evaluator::new().evaluate(&ast, &data).unwrap();
20//! ```
21//!
22//! ## Architecture
23//!
24//! - `parser` - Expression parser (converts JSONata strings to AST)
25//! - `evaluator` - Expression evaluator (executes AST against data)
26//! - `functions` - Built-in function implementations
27//! - `datetime` - Date/time handling functions
28//! - `signature` - Function signature validation
29//! - `ast` - Abstract Syntax Tree definitions
30//! - `value` - JValue type (the runtime value representation)
31
32pub mod ast;
33pub mod ast_transform;
34mod compiler;
35mod datetime;
36pub mod evaluator;
37pub mod functions;
38pub mod parser;
39mod signature;
40pub mod value;
41mod vm;
42
43// ── Benchmarking facade (only when the "bench" feature is enabled) ────────────
44//
45// Exposes the compiler/VM pipeline for Criterion benchmarks without making
46// the internals part of the permanent public API.
47
48/// Internal benchmarking API — do not use in production code.
49///
50/// Enabled with `--features bench`. Provides access to the bytecode compiler
51/// and VM so that Criterion benchmarks can measure tree-walker vs VM directly.
52#[cfg(feature = "bench")]
53pub mod _bench {
54    use crate::ast::AstNode;
55    pub use crate::evaluator::EvaluatorError;
56    use crate::value::JValue;
57
58    /// An opaque handle to a compiled bytecode program.
59    pub struct CompiledProgram(crate::vm::BytecodeProgram);
60
61    /// Compile an AST node to bytecode.
62    ///
63    /// Returns `None` if the expression contains constructs the compiler
64    /// doesn't handle (e.g. wildcards, `$eval`, higher-order functions at
65    /// the top level). In that case, fall back to `Evaluator::new().evaluate()`.
66    pub fn compile(ast: &AstNode) -> Option<CompiledProgram> {
67        crate::evaluator::try_compile_expr(ast)
68            .map(|ce| CompiledProgram(crate::compiler::BytecodeCompiler::compile(&ce)))
69    }
70
71    /// Execute a compiled program against `data`.
72    pub fn run(prog: &CompiledProgram, data: &JValue) -> Result<JValue, EvaluatorError> {
73        crate::vm::Vm::with_options(&prog.0, crate::evaluator::EvaluatorOptions::default())
74            .run(data, None)
75    }
76}
77
78// ── Python bindings (only when the "python" feature is enabled) ───────────────
79
80/// The JSONata reference implementation version this library targets.
81const JSONATA_REFERENCE_VERSION: &str = "2.1.0";
82
83#[cfg(feature = "python")]
84use crate::value::JValue;
85#[cfg(feature = "python")]
86use indexmap::IndexMap;
87#[cfg(feature = "python")]
88use pyo3::exceptions::{PyTypeError, PyValueError};
89#[cfg(feature = "python")]
90use pyo3::prelude::*;
91#[cfg(feature = "python")]
92use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString};
93
94/// Pre-converted data handle for efficient repeated evaluation.
95///
96/// Convert Python data to an internal representation once, then reuse it
97/// across multiple evaluations to avoid repeated Python↔Rust conversion overhead.
98///
99/// # Examples
100///
101/// ```python
102/// import jsonatapy
103///
104/// data = jsonatapy.JsonataData({"orders": [{"price": 150}, {"price": 50}]})
105/// expr = jsonatapy.compile("orders[price > 100]")
106/// result = expr.evaluate_with_data(data)
107/// ```
108#[cfg(feature = "python")]
109#[pyclass(unsendable)]
110struct JsonataData {
111    data: JValue,
112}
113
114#[cfg(feature = "python")]
115#[pymethods]
116impl JsonataData {
117    /// Create from a Python object (dict, list, etc.)
118    #[new]
119    fn new(py: Python, data: Py<PyAny>) -> PyResult<Self> {
120        let jvalue = python_to_json(py, &data)?;
121        Ok(JsonataData { data: jvalue })
122    }
123
124    /// Create from a JSON string (fastest path).
125    #[staticmethod]
126    fn from_json(json_str: &str) -> PyResult<Self> {
127        let data = JValue::from_json_str(json_str)
128            .map_err(|e| PyValueError::new_err(format!("Invalid JSON: {}", e)))?;
129        Ok(JsonataData { data })
130    }
131}
132
133/// A compiled JSONata expression that can be evaluated against data.
134///
135/// This is the main entry point for using JSONata. Compile an expression once,
136/// then evaluate it multiple times against different data.
137///
138/// # Examples
139///
140/// ```python
141/// import jsonatapy
142///
143/// # Compile once
144/// expr = jsonatapy.compile("orders[price > 100].product")
145///
146/// # Evaluate many times
147/// data1 = {"orders": [{"product": "A", "price": 150}]}
148/// result1 = expr.evaluate(data1)
149///
150/// data2 = {"orders": [{"product": "B", "price": 50}]}
151/// result2 = expr.evaluate(data2)
152/// ```
153#[cfg(feature = "python")]
154#[pyclass(unsendable)]
155struct JsonataExpression {
156    /// The parsed Abstract Syntax Tree
157    ast: ast::AstNode,
158    /// Lazily compiled bytecode — populated on first evaluate() call.
159    /// `Some(bc)` = fast VM path; `None` = must use tree-walker.
160    /// `OnceCell` ensures compilation happens at most once per expression instance.
161    bytecode: std::cell::OnceCell<Option<vm::BytecodeProgram>>,
162    /// Default guardrail options set at `compile()` time. Per-call `evaluate*()`
163    /// kwargs override these on a field-by-field basis (see the `.or(...)` merges
164    /// in the `#[pymethods]` impl below).
165    default_options: evaluator::EvaluatorOptions,
166}
167
168#[cfg(feature = "python")]
169impl JsonataExpression {
170    /// Evaluate the compiled expression against pre-converted data.
171    /// Uses bytecode VM when available, falls back to tree-walker.
172    fn run_eval(
173        &self,
174        py: Python,
175        data: &JValue,
176        bindings: Option<Py<PyAny>>,
177        options: evaluator::EvaluatorOptions,
178    ) -> PyResult<JValue> {
179        if bindings.is_none() {
180            let bytecode = self.bytecode.get_or_init(|| {
181                evaluator::try_compile_expr(&self.ast)
182                    .map(|ce| compiler::BytecodeCompiler::compile(&ce))
183            });
184            if let Some(bc) = bytecode {
185                vm::Vm::with_options(bc, options.clone())
186                    .run(data, None)
187                    .map_err(evaluator_error_to_py)
188            } else {
189                let mut ev = evaluator::Evaluator::with_options(evaluator::Context::new(), options);
190                ev.evaluate(&self.ast, data).map_err(evaluator_error_to_py)
191            }
192        } else {
193            let mut ev = create_evaluator(py, bindings, options)?;
194            ev.evaluate(&self.ast, data).map_err(evaluator_error_to_py)
195        }
196    }
197}
198
199#[cfg(feature = "python")]
200#[pymethods]
201impl JsonataExpression {
202    /// Returns ValueError if evaluation fails
203    #[pyo3(signature = (data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
204    #[allow(clippy::too_many_arguments)]
205    fn evaluate(
206        &self,
207        py: Python,
208        data: Py<PyAny>,
209        bindings: Option<Py<PyAny>>,
210        timeout: Option<u64>,
211        max_stack_depth: Option<usize>,
212        max_sequence_length: Option<usize>,
213    ) -> PyResult<Py<PyAny>> {
214        let json_data = python_to_json(py, &data)?;
215        let options = evaluator::EvaluatorOptions {
216            timeout_ms: timeout.or(self.default_options.timeout_ms),
217            max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
218            max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
219        };
220        json_to_python(py, &self.run_eval(py, &json_data, bindings, options)?)
221    }
222
223    /// Evaluate with a pre-converted data handle (fastest for repeated evaluation).
224    ///
225    /// # Arguments
226    ///
227    /// * `data` - A JsonataData handle (pre-converted from Python to internal format)
228    /// * `bindings` - Optional additional variable bindings
229    ///
230    /// # Returns
231    ///
232    /// The result of evaluating the expression
233    #[pyo3(signature = (data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
234    #[allow(clippy::too_many_arguments)]
235    fn evaluate_with_data(
236        &self,
237        py: Python,
238        data: &JsonataData,
239        bindings: Option<Py<PyAny>>,
240        timeout: Option<u64>,
241        max_stack_depth: Option<usize>,
242        max_sequence_length: Option<usize>,
243    ) -> PyResult<Py<PyAny>> {
244        let options = evaluator::EvaluatorOptions {
245            timeout_ms: timeout.or(self.default_options.timeout_ms),
246            max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
247            max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
248        };
249        json_to_python(py, &self.run_eval(py, &data.data, bindings, options)?)
250    }
251
252    /// Evaluate with a pre-converted data handle, return JSON string (zero-overhead output).
253    ///
254    /// # Arguments
255    ///
256    /// * `data` - A JsonataData handle (pre-converted from Python to internal format)
257    /// * `bindings` - Optional additional variable bindings
258    ///
259    /// # Returns
260    ///
261    /// The result as a JSON string
262    #[pyo3(signature = (data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
263    #[allow(clippy::too_many_arguments)]
264    fn evaluate_data_to_json(
265        &self,
266        py: Python,
267        data: &JsonataData,
268        bindings: Option<Py<PyAny>>,
269        timeout: Option<u64>,
270        max_stack_depth: Option<usize>,
271        max_sequence_length: Option<usize>,
272    ) -> PyResult<String> {
273        let options = evaluator::EvaluatorOptions {
274            timeout_ms: timeout.or(self.default_options.timeout_ms),
275            max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
276            max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
277        };
278        self.run_eval(py, &data.data, bindings, options)?
279            .to_json_string()
280            .map_err(|e| PyValueError::new_err(format!("Failed to serialize result: {}", e)))
281    }
282
283    /// Evaluate the expression with JSON string input/output (faster for large data).
284    ///
285    /// This method avoids Python↔Rust conversion overhead by accepting and returning
286    /// JSON strings directly. This is significantly faster for large datasets.
287    ///
288    /// # Arguments
289    ///
290    /// * `json_str` - Input data as a JSON string
291    /// * `bindings` - Optional dict of variable bindings (default: None)
292    ///
293    /// # Returns
294    ///
295    /// The result as a JSON string
296    ///
297    /// # Errors
298    ///
299    /// Returns ValueError if JSON parsing or evaluation fails
300    #[pyo3(signature = (json_str, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
301    #[allow(clippy::too_many_arguments)]
302    fn evaluate_json(
303        &self,
304        py: Python,
305        json_str: &str,
306        bindings: Option<Py<PyAny>>,
307        timeout: Option<u64>,
308        max_stack_depth: Option<usize>,
309        max_sequence_length: Option<usize>,
310    ) -> PyResult<String> {
311        let json_data = JValue::from_json_str(json_str)
312            .map_err(|e| PyValueError::new_err(format!("Invalid JSON: {}", e)))?;
313        let options = evaluator::EvaluatorOptions {
314            timeout_ms: timeout.or(self.default_options.timeout_ms),
315            max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
316            max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
317        };
318        self.run_eval(py, &json_data, bindings, options)?
319            .to_json_string()
320            .map_err(|e| PyValueError::new_err(format!("Failed to serialize result: {}", e)))
321    }
322
323    /// Evaluate with JSON string input, distinguishing an Undefined result
324    /// (returns Python None) from an explicit JSON null result (returns
325    /// the string "null"). evaluate_json() cannot make this distinction --
326    /// JSON serialization has no way to represent "undefined" separately
327    /// from "null" -- so this method checks the raw evaluated JValue's
328    /// is_undefined() BEFORE serializing, exposing the same signal the
329    /// Rust CLI (src/bin/jsonata/main.rs) already uses internally.
330    ///
331    /// `json_str=None` means no input document at all -- the top-level
332    /// context (`$`) is bound to a true `Undefined`, matching the Rust
333    /// CLI's `--null-input` behavior. This is distinct from passing the
334    /// text `"null"`, which binds `$` to an explicit JSON null.
335    ///
336    /// # Errors
337    ///
338    /// Returns ValueError if JSON parsing or evaluation fails
339    #[pyo3(signature = (json_str, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
340    #[allow(clippy::too_many_arguments)]
341    fn evaluate_json_or_none(
342        &self,
343        py: Python,
344        json_str: Option<&str>,
345        bindings: Option<Py<PyAny>>,
346        timeout: Option<u64>,
347        max_stack_depth: Option<usize>,
348        max_sequence_length: Option<usize>,
349    ) -> PyResult<Option<String>> {
350        let json_data = match json_str {
351            Some(s) => JValue::from_json_str(s)
352                .map_err(|e| PyValueError::new_err(format!("Invalid JSON: {}", e)))?,
353            None => JValue::Undefined,
354        };
355        let options = evaluator::EvaluatorOptions {
356            timeout_ms: timeout.or(self.default_options.timeout_ms),
357            max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
358            max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
359        };
360        let result = self.run_eval(py, &json_data, bindings, options)?;
361        if result.is_undefined() {
362            return Ok(None);
363        }
364        result
365            .to_json_string()
366            .map(Some)
367            .map_err(|e| PyValueError::new_err(format!("Failed to serialize result: {}", e)))
368    }
369}
370
371/// Compile a JSONata expression into an executable form.
372///
373/// # Arguments
374///
375/// * `expression` - A JSONata query/transformation expression string
376///
377/// # Returns
378///
379/// A compiled JsonataExpression that can be evaluated
380///
381/// # Errors
382///
383/// Returns ValueError if the expression cannot be parsed
384///
385/// # Examples
386///
387/// ```python
388/// import jsonatapy
389///
390/// expr = jsonatapy.compile("$.name")
391/// result = expr.evaluate({"name": "Alice"})
392/// print(result)  # "Alice"
393/// ```
394#[cfg(feature = "python")]
395#[pyfunction]
396#[pyo3(signature = (expression, timeout=None, max_stack_depth=None, max_sequence_length=None))]
397fn compile(
398    expression: &str,
399    timeout: Option<u64>,
400    max_stack_depth: Option<usize>,
401    max_sequence_length: Option<usize>,
402) -> PyResult<JsonataExpression> {
403    let ast = parser::parse(expression).map_err(parser_error_to_py)?;
404
405    Ok(JsonataExpression {
406        ast,
407        bytecode: std::cell::OnceCell::new(),
408        default_options: build_evaluator_options(timeout, max_stack_depth, max_sequence_length),
409    })
410}
411
412/// Evaluate a JSONata expression against data in one step.
413///
414/// This is a convenience function that compiles and evaluates in one call.
415/// For repeated evaluations of the same expression, use `compile()` instead.
416///
417/// # Arguments
418///
419/// * `expression` - A JSONata query/transformation expression string
420/// * `data` - A Python object (typically dict) to query/transform
421/// * `bindings` - Optional additional variable bindings
422///
423/// # Returns
424///
425/// The result of evaluating the expression
426///
427/// # Errors
428///
429/// Returns ValueError if parsing or evaluation fails
430///
431/// # Examples
432///
433/// ```python
434/// import jsonatapy
435///
436/// result = jsonatapy.evaluate("$uppercase(name)", {"name": "alice"})
437/// print(result)  # "ALICE"
438/// ```
439#[cfg(feature = "python")]
440#[pyfunction]
441#[pyo3(signature = (expression, data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
442#[allow(clippy::too_many_arguments)]
443fn evaluate(
444    py: Python,
445    expression: &str,
446    data: Py<PyAny>,
447    bindings: Option<Py<PyAny>>,
448    timeout: Option<u64>,
449    max_stack_depth: Option<usize>,
450    max_sequence_length: Option<usize>,
451) -> PyResult<Py<PyAny>> {
452    let expr = compile(expression, None, None, None)?;
453    expr.evaluate(
454        py,
455        data,
456        bindings,
457        timeout,
458        max_stack_depth,
459        max_sequence_length,
460    )
461}
462
463/// Convert a Python object to a JValue.
464///
465/// Handles conversion of Python types:
466/// - None -> Null
467/// - bool -> Bool (checked before int since bool is a subclass of int)
468/// - int, float -> Number
469/// - str -> String
470/// - list -> Array
471/// - dict -> Object
472#[cfg(feature = "python")]
473fn python_to_json(py: Python, obj: &Py<PyAny>) -> PyResult<JValue> {
474    python_to_json_bound(obj.bind(py))
475}
476
477/// Inner conversion using Bound API for zero-overhead type checks.
478///
479/// Uses is_instance_of::<T>() which compiles to C-level type pointer comparisons
480/// (PyBool_Check, PyLong_Check, etc.) — single pointer comparison vs qualname()
481/// which allocates a Python string and does string comparison.
482#[cfg(feature = "python")]
483fn python_to_json_bound(obj: &Bound<'_, PyAny>) -> PyResult<JValue> {
484    if obj.is_none() {
485        return Ok(JValue::Null);
486    }
487
488    // Check bool before int — Python bool is a subclass of int
489    if obj.is_instance_of::<PyBool>() {
490        return Ok(JValue::Bool(obj.extract::<bool>()?));
491    }
492    if obj.is_instance_of::<PyInt>() {
493        return Ok(JValue::Number(obj.extract::<i64>()? as f64));
494    }
495    if obj.is_instance_of::<PyFloat>() {
496        return Ok(JValue::Number(obj.extract::<f64>()?));
497    }
498    if obj.is_instance_of::<PyString>() {
499        return Ok(JValue::string(obj.extract::<String>()?));
500    }
501    if let Ok(list) = obj.cast::<PyList>() {
502        let mut result = Vec::with_capacity(list.len());
503        for item in list.iter() {
504            result.push(python_to_json_bound(&item)?);
505        }
506        return Ok(JValue::array(result));
507    }
508    if let Ok(dict) = obj.cast::<PyDict>() {
509        let mut result = IndexMap::with_capacity(dict.len());
510        for (key, value) in dict.iter() {
511            let key_str = key.extract::<String>()?;
512            result.insert(key_str, python_to_json_bound(&value)?);
513        }
514        return Ok(JValue::object(result));
515    }
516
517    // Fallback for subclasses, numpy types, etc.
518    if let Ok(b) = obj.extract::<bool>() {
519        return Ok(JValue::Bool(b));
520    }
521    if let Ok(i) = obj.extract::<i64>() {
522        return Ok(JValue::Number(i as f64));
523    }
524    if let Ok(f) = obj.extract::<f64>() {
525        return Ok(JValue::Number(f));
526    }
527    if let Ok(s) = obj.extract::<String>() {
528        return Ok(JValue::string(s));
529    }
530
531    Err(PyTypeError::new_err(format!(
532        "Cannot convert Python object to JSON: {}",
533        obj.get_type().name()?
534    )))
535}
536
537/// Convert a JValue to a Python object.
538///
539/// Handles conversion of JValue variants to Python types:
540/// - Null/Undefined -> None
541/// - Bool -> bool
542/// - Number -> int (if whole number) or float
543/// - String -> str
544/// - Array -> list (batch-constructed via PyList::new for fewer C API calls)
545/// - Object -> dict
546/// - Lambda/Builtin/Regex -> None
547#[cfg(feature = "python")]
548fn json_to_python(py: Python, value: &JValue) -> PyResult<Py<PyAny>> {
549    match value {
550        JValue::Null | JValue::Undefined => Ok(py.None()),
551
552        JValue::Bool(b) => Ok(b.into_pyobject(py).unwrap().to_owned().into_any().unbind()),
553
554        JValue::Number(n) => {
555            // If it's a whole number that fits in i64, return as Python int
556            if n.fract() == 0.0 && n.is_finite() && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
557                Ok((*n as i64).into_pyobject(py).unwrap().into_any().unbind())
558            } else {
559                Ok(n.into_pyobject(py).unwrap().into_any().unbind())
560            }
561        }
562
563        JValue::String(s) => Ok((&**s).into_pyobject(py).unwrap().into_any().unbind()),
564
565        JValue::Array(arr) => {
566            // Array of objects with shared keys: intern first object's keys as
567            // Python strings to avoid repeated UTF-8 -> PyString conversion.
568            let all_objects =
569                arr.len() >= 2 && arr.iter().all(|item| matches!(item, JValue::Object(_)));
570            if all_objects {
571                let first_obj = match arr.first() {
572                    Some(JValue::Object(obj)) => obj,
573                    _ => unreachable!("all_objects guard ensures first element is an object"),
574                };
575
576                // Intern keys: store (&str, Py<PyString>) — no String clone needed
577                // since first_obj borrows from arr which outlives this block
578                let interned_keys: Vec<(&str, Py<PyString>)> = first_obj
579                    .keys()
580                    .map(|k| (k.as_str(), PyString::new(py, k).unbind()))
581                    .collect();
582
583                let items: Vec<Py<PyAny>> = arr
584                    .iter()
585                    .map(|item| {
586                        // Safe to unwrap: all_objects guarantees every element is Object
587                        let obj = match item {
588                            JValue::Object(obj) => obj,
589                            _ => unreachable!(),
590                        };
591                        let dict = PyDict::new(py);
592                        for (key_str, py_key) in &interned_keys {
593                            if let Some(value) = obj.get(*key_str) {
594                                dict.set_item(py_key.bind(py), json_to_python(py, value)?)?;
595                            }
596                        }
597                        // Handle any extra keys not in first object
598                        for (key, value) in obj.iter() {
599                            if !first_obj.contains_key(key) {
600                                dict.set_item(key, json_to_python(py, value)?)?;
601                            }
602                        }
603                        Ok(dict.unbind().into())
604                    })
605                    .collect::<PyResult<Vec<_>>>()?;
606                return Ok(PyList::new(py, &items)?.unbind().into());
607            }
608
609            // General array: batch construction
610            let items: Vec<Py<PyAny>> = arr
611                .iter()
612                .map(|item| json_to_python(py, item))
613                .collect::<PyResult<Vec<_>>>()?;
614            Ok(PyList::new(py, &items)?.unbind().into())
615        }
616
617        JValue::Object(obj) => {
618            let dict = PyDict::new(py);
619            for (key, value) in obj.iter() {
620                dict.set_item(key, json_to_python(py, value)?)?;
621            }
622            Ok(dict.unbind().into())
623        }
624
625        JValue::Lambda { .. } | JValue::Builtin { .. } | JValue::Regex { .. } => Ok(py.None()),
626    }
627}
628
629/// Build an `EvaluatorOptions` from the Python-facing `timeout`/`max_stack_depth`/
630/// `max_sequence_length` kwargs used by `compile()` and the free-standing `evaluate()`.
631#[cfg(feature = "python")]
632fn build_evaluator_options(
633    timeout: Option<u64>,
634    max_stack_depth: Option<usize>,
635    max_sequence_length: Option<usize>,
636) -> evaluator::EvaluatorOptions {
637    evaluator::EvaluatorOptions {
638        timeout_ms: timeout,
639        max_stack_depth,
640        max_sequence_length,
641    }
642}
643
644/// Create an evaluator, optionally configured with Python bindings
645#[cfg(feature = "python")]
646fn create_evaluator(
647    py: Python,
648    bindings: Option<Py<PyAny>>,
649    options: evaluator::EvaluatorOptions,
650) -> PyResult<evaluator::Evaluator> {
651    let mut context = evaluator::Context::new();
652    if let Some(bindings_obj) = bindings {
653        let bindings_json = python_to_json(py, &bindings_obj)?;
654        if let JValue::Object(map) = bindings_json {
655            for (key, value) in map.iter() {
656                context.bind(key.clone(), value.clone());
657            }
658        } else {
659            return Err(PyTypeError::new_err("bindings must be a dictionary"));
660        }
661    }
662    Ok(evaluator::Evaluator::with_options(context, options))
663}
664
665/// Convert an EvaluatorError to a PyErr
666#[cfg(feature = "python")]
667fn evaluator_error_to_py(e: evaluator::EvaluatorError) -> PyErr {
668    PyValueError::new_err(e.message().to_string())
669}
670
671/// Convert a ParserError to a PyErr
672#[cfg(feature = "python")]
673fn parser_error_to_py(e: parser::ParserError) -> PyErr {
674    PyValueError::new_err(e.display_message())
675}
676
677/// JSONata Python module
678#[cfg(feature = "python")]
679#[pymodule]
680fn _jsonatapy(m: &Bound<'_, PyModule>) -> PyResult<()> {
681    m.add_function(wrap_pyfunction!(compile, m)?)?;
682    m.add_function(wrap_pyfunction!(evaluate, m)?)?;
683    m.add_class::<JsonataExpression>()?;
684    m.add_class::<JsonataData>()?;
685
686    // Add version info
687    m.add("__version__", env!("CARGO_PKG_VERSION"))?;
688    m.add("__jsonata_version__", JSONATA_REFERENCE_VERSION)?;
689
690    Ok(())
691}
692
693#[cfg(test)]
694mod tests {
695    #[test]
696    fn test_module_creation() {
697        // Basic smoke test
698        assert!(!env!("CARGO_PKG_VERSION").is_empty());
699    }
700
701    mod parser_error_handling {
702        use super::super::*;
703
704        // These test ParserError::display_message() directly (a plain string
705        // method) rather than parser_error_to_py(), which constructs a
706        // PyErr -- that requires an initialized Python interpreter, which
707        // isn't available under a bare `cargo test --all-features` run (no
708        // embedded interpreter, unlike the maturin-built extension loaded
709        // into a live Python process). The formatting logic under test is
710        // identical either way.
711
712        #[test]
713        fn test_parser_error_to_py_coded_error_no_prefix() {
714            // Test that coded errors (like S0214) are passed through without "Parse error: " prefix
715            let coded_error = parser::ParserError::Coded {
716                code: "S0214",
717                message: "Expected a variable reference after @".to_string(),
718            };
719            let msg = coded_error.display_message();
720
721            // The message should start with the code, not "Parse error: "
722            assert!(
723                msg.starts_with("S0214:"),
724                "Expected message to start with 'S0214:', got: {}",
725                msg
726            );
727            assert!(
728                !msg.starts_with("Parse error:"),
729                "Expected no 'Parse error:' prefix, got: {}",
730                msg
731            );
732        }
733
734        #[test]
735        fn test_parser_error_to_py_non_coded_error_with_prefix() {
736            // Test that non-coded errors still get the "Parse error: " prefix
737            let non_coded_error = parser::ParserError::UnexpectedToken("foo".to_string());
738            let msg = non_coded_error.display_message();
739
740            // The message should have the "Parse error: " prefix
741            assert!(
742                msg.starts_with("Parse error:"),
743                "Expected message to start with 'Parse error:', got: {}",
744                msg
745            );
746        }
747    }
748}