pub mod ast;
pub mod ast_transform;
mod compiler;
mod datetime;
pub mod evaluator;
pub mod functions;
#[cfg(feature = "python")]
pub mod lazy;
pub mod parser;
mod signature;
pub mod value;
mod vm;
#[cfg(feature = "bench")]
pub mod _bench {
use crate::ast::AstNode;
pub use crate::evaluator::EvaluatorError;
use crate::value::JValue;
pub struct CompiledProgram(crate::vm::BytecodeProgram);
pub fn compile(ast: &AstNode) -> Option<CompiledProgram> {
crate::evaluator::try_compile_expr(ast)
.map(|ce| CompiledProgram(crate::compiler::BytecodeCompiler::compile(&ce)))
}
pub fn run(prog: &CompiledProgram, data: &JValue) -> Result<JValue, EvaluatorError> {
crate::vm::Vm::with_options(&prog.0, crate::evaluator::EvaluatorOptions::default())
.run(data, None)
}
}
const JSONATA_REFERENCE_VERSION: &str = "2.1.0";
#[cfg(feature = "python")]
use crate::value::JValue;
#[cfg(feature = "python")]
use pyo3::exceptions::{PyTypeError, PyValueError};
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use pyo3::types::{PyDict, PyList, PyString};
#[cfg(feature = "python")]
#[pyclass(unsendable)]
struct JsonataData {
data: JValue,
}
#[cfg(feature = "python")]
#[pymethods]
impl JsonataData {
#[new]
fn new(py: Python, data: Py<PyAny>) -> PyResult<Self> {
let jvalue = python_to_json(py, &data)?;
Ok(JsonataData { data: jvalue })
}
#[staticmethod]
fn from_json(json_str: &str) -> PyResult<Self> {
let data = JValue::from_json_str(json_str)
.map_err(|e| PyValueError::new_err(format!("Invalid JSON: {}", e)))?;
Ok(JsonataData { data })
}
}
#[cfg(feature = "python")]
#[pyclass(unsendable)]
struct JsonataExpression {
ast: ast::AstNode,
bytecode: std::cell::OnceCell<Option<vm::BytecodeProgram>>,
default_options: evaluator::EvaluatorOptions,
}
#[cfg(feature = "python")]
fn force_tree_walker() -> bool {
std::env::var_os("JSONATAPY_FORCE_TREE_WALKER").is_some_and(|v| !v.is_empty() && v != "0")
}
#[cfg(feature = "python")]
impl JsonataExpression {
fn run_eval(
&self,
py: Python,
data: &JValue,
bindings: Option<Py<PyAny>>,
options: evaluator::EvaluatorOptions,
) -> PyResult<JValue> {
if bindings.is_none() && !force_tree_walker() {
let bytecode = self.bytecode.get_or_init(|| {
evaluator::try_compile_expr(&self.ast)
.map(|ce| compiler::BytecodeCompiler::compile(&ce))
});
if let Some(bc) = bytecode {
vm::Vm::with_options(bc, options.clone())
.run(data, None)
.map_err(evaluator_error_to_py)
} else {
let mut ev = evaluator::Evaluator::with_options(evaluator::Context::new(), options);
ev.evaluate(&self.ast, data).map_err(evaluator_error_to_py)
}
} else {
let mut ev = create_evaluator(py, bindings, options)?;
ev.evaluate(&self.ast, data).map_err(evaluator_error_to_py)
}
}
}
#[cfg(feature = "python")]
#[pymethods]
impl JsonataExpression {
#[pyo3(signature = (data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
#[allow(clippy::too_many_arguments)]
fn evaluate(
&self,
py: Python,
data: Py<PyAny>,
bindings: Option<Py<PyAny>>,
timeout: Option<u64>,
max_stack_depth: Option<usize>,
max_sequence_length: Option<usize>,
) -> PyResult<Py<PyAny>> {
let json_data = lazy::convert(data.bind(py), true)?;
let options = evaluator::EvaluatorOptions {
timeout_ms: timeout.or(self.default_options.timeout_ms),
max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
};
json_to_python(py, &self.run_eval(py, &json_data, bindings, options)?)
}
#[pyo3(signature = (data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
#[allow(clippy::too_many_arguments)]
fn evaluate_with_data(
&self,
py: Python,
data: &JsonataData,
bindings: Option<Py<PyAny>>,
timeout: Option<u64>,
max_stack_depth: Option<usize>,
max_sequence_length: Option<usize>,
) -> PyResult<Py<PyAny>> {
let options = evaluator::EvaluatorOptions {
timeout_ms: timeout.or(self.default_options.timeout_ms),
max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
};
json_to_python(py, &self.run_eval(py, &data.data, bindings, options)?)
}
#[pyo3(signature = (data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
#[allow(clippy::too_many_arguments)]
fn evaluate_data_to_json(
&self,
py: Python,
data: &JsonataData,
bindings: Option<Py<PyAny>>,
timeout: Option<u64>,
max_stack_depth: Option<usize>,
max_sequence_length: Option<usize>,
) -> PyResult<String> {
let options = evaluator::EvaluatorOptions {
timeout_ms: timeout.or(self.default_options.timeout_ms),
max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
};
self.run_eval(py, &data.data, bindings, options)?
.to_json_string()
.map_err(|e| PyValueError::new_err(format!("Failed to serialize result: {}", e)))
}
#[pyo3(signature = (json_str, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
#[allow(clippy::too_many_arguments)]
fn evaluate_json(
&self,
py: Python,
json_str: &str,
bindings: Option<Py<PyAny>>,
timeout: Option<u64>,
max_stack_depth: Option<usize>,
max_sequence_length: Option<usize>,
) -> PyResult<String> {
let json_data = JValue::from_json_str(json_str)
.map_err(|e| PyValueError::new_err(format!("Invalid JSON: {}", e)))?;
let options = evaluator::EvaluatorOptions {
timeout_ms: timeout.or(self.default_options.timeout_ms),
max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
};
self.run_eval(py, &json_data, bindings, options)?
.to_json_string()
.map_err(|e| PyValueError::new_err(format!("Failed to serialize result: {}", e)))
}
#[pyo3(signature = (json_str, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
#[allow(clippy::too_many_arguments)]
fn evaluate_json_or_none(
&self,
py: Python,
json_str: Option<&str>,
bindings: Option<Py<PyAny>>,
timeout: Option<u64>,
max_stack_depth: Option<usize>,
max_sequence_length: Option<usize>,
) -> PyResult<Option<String>> {
let json_data = match json_str {
Some(s) => JValue::from_json_str(s)
.map_err(|e| PyValueError::new_err(format!("Invalid JSON: {}", e)))?,
None => JValue::Undefined,
};
let options = evaluator::EvaluatorOptions {
timeout_ms: timeout.or(self.default_options.timeout_ms),
max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
};
let result = self.run_eval(py, &json_data, bindings, options)?;
if result.is_undefined() {
return Ok(None);
}
result
.to_json_string()
.map(Some)
.map_err(|e| PyValueError::new_err(format!("Failed to serialize result: {}", e)))
}
}
#[cfg(feature = "python")]
#[pyfunction]
#[pyo3(signature = (expression, timeout=None, max_stack_depth=None, max_sequence_length=None))]
fn compile(
expression: &str,
timeout: Option<u64>,
max_stack_depth: Option<usize>,
max_sequence_length: Option<usize>,
) -> PyResult<JsonataExpression> {
let ast = parser::parse(expression).map_err(parser_error_to_py)?;
Ok(JsonataExpression {
ast,
bytecode: std::cell::OnceCell::new(),
default_options: build_evaluator_options(timeout, max_stack_depth, max_sequence_length),
})
}
#[cfg(feature = "python")]
#[pyfunction]
#[pyo3(signature = (expression, data, bindings=None, timeout=None, max_stack_depth=None, max_sequence_length=None))]
#[allow(clippy::too_many_arguments)]
fn evaluate(
py: Python,
expression: &str,
data: Py<PyAny>,
bindings: Option<Py<PyAny>>,
timeout: Option<u64>,
max_stack_depth: Option<usize>,
max_sequence_length: Option<usize>,
) -> PyResult<Py<PyAny>> {
let expr = compile(expression, None, None, None)?;
expr.evaluate(
py,
data,
bindings,
timeout,
max_stack_depth,
max_sequence_length,
)
}
#[cfg(feature = "python")]
fn python_to_json(py: Python, obj: &Py<PyAny>) -> PyResult<JValue> {
python_to_json_bound(obj.bind(py))
}
#[cfg(feature = "python")]
fn python_to_json_bound(obj: &Bound<'_, PyAny>) -> PyResult<JValue> {
lazy::convert(obj, false)
}
#[cfg(feature = "python")]
fn json_to_python(py: Python, value: &JValue) -> PyResult<Py<PyAny>> {
match value {
JValue::Null | JValue::Undefined => Ok(py.None()),
JValue::Bool(b) => Ok(b.into_pyobject(py).unwrap().to_owned().into_any().unbind()),
JValue::Number(n) => {
if n.fract() == 0.0 && n.is_finite() && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
Ok((*n as i64).into_pyobject(py).unwrap().into_any().unbind())
} else {
Ok(n.into_pyobject(py).unwrap().into_any().unbind())
}
}
JValue::String(s) => Ok((&**s).into_pyobject(py).unwrap().into_any().unbind()),
JValue::Array(arr) => {
let all_objects =
arr.len() >= 2 && arr.iter().all(|item| matches!(item, JValue::Object(_)));
if all_objects {
let first_obj = match arr.first() {
Some(JValue::Object(obj)) => obj,
_ => unreachable!("all_objects guard ensures first element is an object"),
};
let interned_keys: Vec<(&str, Py<PyString>)> = first_obj
.keys()
.map(|k| (k.as_str(), PyString::new(py, k).unbind()))
.collect();
let items: Vec<Py<PyAny>> = arr
.iter()
.map(|item| {
let obj = match item {
JValue::Object(obj) => obj,
_ => unreachable!(),
};
let dict = PyDict::new(py);
for (key_str, py_key) in &interned_keys {
if let Some(value) = obj.get(*key_str) {
dict.set_item(py_key.bind(py), json_to_python(py, value)?)?;
}
}
for (key, value) in obj.iter() {
if !first_obj.contains_key(key) {
dict.set_item(key, json_to_python(py, value)?)?;
}
}
Ok(dict.unbind().into())
})
.collect::<PyResult<Vec<_>>>()?;
return Ok(PyList::new(py, &items)?.unbind().into());
}
let items: Vec<Py<PyAny>> = arr
.iter()
.map(|item| json_to_python(py, item))
.collect::<PyResult<Vec<_>>>()?;
Ok(PyList::new(py, &items)?.unbind().into())
}
JValue::Object(obj) => {
let dict = PyDict::new(py);
for (key, value) in obj.iter() {
dict.set_item(key, json_to_python(py, value)?)?;
}
Ok(dict.unbind().into())
}
JValue::Lambda { .. } | JValue::Builtin { .. } | JValue::Regex { .. } => Ok(py.None()),
JValue::LazyPyDict(lazy) => Ok(lazy.py_object().clone_ref(py).into_any()),
}
}
#[cfg(feature = "python")]
fn build_evaluator_options(
timeout: Option<u64>,
max_stack_depth: Option<usize>,
max_sequence_length: Option<usize>,
) -> evaluator::EvaluatorOptions {
evaluator::EvaluatorOptions {
timeout_ms: timeout,
max_stack_depth,
max_sequence_length,
}
}
#[cfg(feature = "python")]
fn create_evaluator(
py: Python,
bindings: Option<Py<PyAny>>,
options: evaluator::EvaluatorOptions,
) -> PyResult<evaluator::Evaluator> {
let mut context = evaluator::Context::new();
if let Some(bindings_obj) = bindings {
let bindings_json = python_to_json(py, &bindings_obj)?;
if let JValue::Object(map) = bindings_json {
for (key, value) in map.iter() {
context.bind(key.clone(), value.clone());
}
} else {
return Err(PyTypeError::new_err("bindings must be a dictionary"));
}
}
Ok(evaluator::Evaluator::with_options(context, options))
}
#[cfg(feature = "python")]
fn evaluator_error_to_py(e: evaluator::EvaluatorError) -> PyErr {
match e {
evaluator::EvaluatorError::PyConversionError(m) => PyTypeError::new_err(m),
other => PyValueError::new_err(other.message().to_string()),
}
}
#[cfg(feature = "python")]
fn parser_error_to_py(e: parser::ParserError) -> PyErr {
PyValueError::new_err(e.display_message())
}
#[cfg(feature = "python")]
#[pymodule]
fn _jsonatapy(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(compile, m)?)?;
m.add_function(wrap_pyfunction!(evaluate, m)?)?;
m.add_class::<JsonataExpression>()?;
m.add_class::<JsonataData>()?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
m.add("__jsonata_version__", JSONATA_REFERENCE_VERSION)?;
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn test_module_creation() {
assert!(!env!("CARGO_PKG_VERSION").is_empty());
}
mod parser_error_handling {
use super::super::*;
#[test]
fn test_parser_error_to_py_coded_error_no_prefix() {
let coded_error = parser::ParserError::Coded {
code: "S0214",
message: "Expected a variable reference after @".to_string(),
};
let msg = coded_error.display_message();
assert!(
msg.starts_with("S0214:"),
"Expected message to start with 'S0214:', got: {}",
msg
);
assert!(
!msg.starts_with("Parse error:"),
"Expected no 'Parse error:' prefix, got: {}",
msg
);
}
#[test]
fn test_parser_error_to_py_non_coded_error_with_prefix() {
let non_coded_error = parser::ParserError::UnexpectedToken("foo".to_string());
let msg = non_coded_error.display_message();
assert!(
msg.starts_with("Parse error:"),
"Expected message to start with 'Parse error:', got: {}",
msg
);
}
}
}