jlens-core 0.2.0

JSONata language engine: parser, evaluator, function library and auditing
Documentation
//! # jlens-core
//!
//! The JSONata language engine behind jlens: lexer, parser, evaluator, function library and
//! the evaluation audit subsystem.
//!
//! Compile an expression once, then evaluate it against any number of JSON documents:
//!
//! ```
//! use jlens_core::{Bindings, Expression};
//! use serde_json::json;
//!
//! let expr = Expression::compile("$sum(items[type = $kind].(price * qty))").unwrap();
//! let input = json!({"items": [
//!     {"type": "a", "price": 2.5, "qty": 4},
//!     {"type": "b", "price": 1.0, "qty": 1},
//! ]});
//! let bindings = Bindings::new().var("kind", "a");
//! assert_eq!(expr.evaluate(Some(&input), &bindings).unwrap(), Some(json!(10)));
//!
//! let err = Expression::compile("Account.Order[").unwrap_err();
//! assert_eq!(err.code(), "S0203");
//! ```
//!
//! For editors, [`parse_recover`] keeps going after syntax errors and returns a partial tree
//! together with [`Diagnostic`]s:
//!
//! ```
//! let ast = jlens_core::parse_recover("Account.");
//! assert!(ast.has_errors());
//! assert_eq!(ast.diagnostics[0].code, "S0207");
//! ```
//!
//! Parts of this crate are ported from [jsonata-js](https://github.com/jsonata-js/jsonata)
//! (© IBM Corp. and JSONata contributors, MIT license); the ported modules say so in their
//! documentation.

pub mod annotation;
pub mod ast;
pub mod audit;
mod datetime;
pub mod debug;
mod document;
pub mod error;
mod eval;
pub mod functions;
mod host;
mod json;
mod lexer;
mod parser;
mod process;
mod regex;
pub mod signature;
pub mod stack;
mod value;

pub use annotation::Annotation;
pub use ast::{Ast, Node, NodeKind, Span};
pub use document::Document;
pub use error::{Diagnostic, Error, Result, Severity};
pub use functions::{Category, FunctionInfo, catalog as function_catalog};
pub use host::{HostCall, HostFunction};
pub use json::format_json;

use std::sync::Arc;
use std::time::Duration;

use indexmap::IndexMap;
use serde_json::Value as JsonValue;

/// Parses an expression, failing on the first syntax error.
///
/// Warnings (such as misplaced annotations) are returned in [`Ast::diagnostics`].
pub fn parse(source: &str) -> Result<Ast> {
    let (raw, errors, warnings) = parser::parse(source, false);
    debug_assert!(errors.is_empty());
    let mut processor = process::Processor::new(source, false);
    let root = processor.process_root(raw?)?;
    Ok(Ast {
        root,
        slots: processor.slots,
        diagnostics: warnings,
        source: source.to_owned(),
    })
}

/// Parses an expression, recovering from syntax errors.
///
/// The returned tree contains [`NodeKind::Error`] nodes where the input could not be parsed,
/// and [`Ast::diagnostics`] lists every error and warning, ordered by position. This is the
/// entry point for editors and language tooling.
pub fn parse_recover(source: &str) -> Ast {
    let (raw, parse_errors, warnings) = parser::parse(source, true);
    let mut processor = process::Processor::new(source, true);
    let root = raw
        .and_then(|raw| processor.process_root(raw))
        .unwrap_or_else(|err| {
            let span = err.span().unwrap_or_default();
            processor.errors.push(err.clone());
            Node::new(
                NodeKind::Error {
                    error: Box::new(err),
                },
                span,
                span.end,
            )
        });
    let mut diagnostics: Vec<Diagnostic> = parse_errors
        .iter()
        .chain(&processor.errors)
        .map(Diagnostic::from)
        .collect();
    diagnostics.extend(warnings);
    diagnostics.sort_by_key(|d| d.span.start);
    Ast {
        root,
        slots: processor.slots,
        diagnostics,
        source: source.to_owned(),
    }
}

/// A compiled JSONata expression.
///
/// Compile once with [`Expression::compile`] and evaluate many times. A compiled expression is
/// immutable and can be shared between threads.
#[derive(Debug, Clone)]
pub struct Expression {
    ast: Arc<Ast>,
}

impl Expression {
    /// Parses and compiles a JSONata expression.
    pub fn compile(source: &str) -> Result<Self> {
        Ok(Expression {
            ast: Arc::new(parse(source)?),
        })
    }

    /// Returns the source text this expression was compiled from.
    pub fn source(&self) -> &str {
        &self.ast.source
    }

    /// The syntax tree of the expression.
    pub fn ast(&self) -> &Ast {
        &self.ast
    }

    /// Evaluates the expression against `input` with default [`Options`].
    ///
    /// `None` as input means *undefined* (no input document). A result of `Ok(None)` means
    /// the expression evaluated to *undefined*.
    pub fn evaluate(
        &self,
        input: Option<&JsonValue>,
        bindings: &Bindings,
    ) -> Result<Option<JsonValue>> {
        self.evaluate_with(input, bindings, &Options::default())
    }

    /// Evaluates the expression with explicit [`Options`] (limits and guardrails).
    pub fn evaluate_with(
        &self,
        input: Option<&JsonValue>,
        bindings: &Bindings,
        options: &Options,
    ) -> Result<Option<JsonValue>> {
        eval::evaluate(
            &self.ast,
            json_input(input),
            bindings,
            limits(options),
            None,
        )
    }

    /// Evaluates the expression against a [`Document`] converted beforehand. Use it to
    /// evaluate several expressions, or the same one several times, against one large document
    /// without converting it each time.
    pub fn evaluate_document(
        &self,
        document: &Document,
        bindings: &Bindings,
        options: &Options,
    ) -> Result<Option<JsonValue>> {
        eval::evaluate(
            &self.ast,
            document.value().clone(),
            bindings,
            limits(options),
            None,
        )
    }

    /// Evaluates the expression, reporting every evaluation event to `tracer`.
    ///
    /// See the [`audit`] module for the events and the tracers jlens provides.
    pub fn evaluate_traced(
        &self,
        input: Option<&JsonValue>,
        bindings: &Bindings,
        options: &Options,
        tracer: &mut dyn audit::Tracer,
    ) -> Result<Option<JsonValue>> {
        eval::evaluate(
            &self.ast,
            json_input(input),
            bindings,
            limits(options),
            Some(tracer),
        )
    }

    /// Evaluates the expression and records an [`audit::AuditTree`] of how the result was
    /// reached. The audit is returned even when the evaluation fails.
    ///
    /// ```
    /// use jlens_core::{Bindings, Expression, Options};
    /// use jlens_core::audit::{AuditOptions, OutcomeKind};
    ///
    /// let expr = Expression::compile("$x > 10").unwrap();
    /// let bindings = Bindings::new().var("x", 3);
    /// let (result, audit) =
    ///     expr.evaluate_audited(None, &bindings, &Options::default(), &AuditOptions::decisions());
    /// assert_eq!(result.unwrap(), Some(serde_json::json!(false)));
    /// let root = audit.root.unwrap();
    /// assert_eq!(root.outcome, OutcomeKind::False);
    /// assert_eq!(root.operands, Some([Some(3.into()), Some(10.into())]));
    /// ```
    pub fn evaluate_audited(
        &self,
        input: Option<&JsonValue>,
        bindings: &Bindings,
        options: &Options,
        audit: &audit::AuditOptions,
    ) -> (Result<Option<JsonValue>>, audit::AuditTree) {
        let mut collector = audit::AuditCollector::new(*audit, input);
        let result = self.evaluate_traced(input, bindings, options, &mut collector);
        (result, collector.finish())
    }
}

fn json_input(input: Option<&JsonValue>) -> value::Value {
    input.map_or(value::Value::Undefined, value::Value::from_json)
}

fn limits(options: &Options) -> eval::Limits {
    eval::Limits {
        max_depth: options.max_depth.unwrap_or(eval::DEFAULT_MAX_DEPTH),
        timeout: options.timeout,
    }
}

/// Variables and host functions made available to an expression as `$name`.
///
/// A name holds either a value or a [`HostFunction`]: binding one replaces the other. Bindings
/// take precedence over the built-in functions of the same name, as in jsonata-js.
#[derive(Debug, Clone, Default)]
pub struct Bindings {
    vars: IndexMap<String, JsonValue>,
    functions: IndexMap<String, HostFunction>,
}

impl Bindings {
    /// Creates an empty set of bindings.
    pub fn new() -> Self {
        Self::default()
    }

    /// Adds (or replaces) a variable binding. The name is given without the leading `$`.
    pub fn var(mut self, name: impl Into<String>, value: impl Into<JsonValue>) -> Self {
        let name = name.into();
        self.functions.shift_remove(&name);
        self.vars.insert(name, value.into());
        self
    }

    /// Adds (or replaces) a host function, called as `$name(...)`. The name is given without
    /// the leading `$`.
    pub fn function(mut self, name: impl Into<String>, function: HostFunction) -> Self {
        let name = name.into();
        self.vars.shift_remove(&name);
        self.functions.insert(name, function);
        self
    }

    /// Looks up a variable binding by name (without the leading `$`).
    pub fn get(&self, name: &str) -> Option<&JsonValue> {
        self.vars.get(name)
    }

    /// Looks up a host function by name (without the leading `$`).
    pub fn get_function(&self, name: &str) -> Option<&HostFunction> {
        self.functions.get(name)
    }

    /// Iterates over the variable bindings in insertion order.
    pub fn iter(&self) -> impl Iterator<Item = (&str, &JsonValue)> {
        self.vars.iter().map(|(k, v)| (k.as_str(), v))
    }

    /// Iterates over the host functions in insertion order.
    pub fn functions(&self) -> impl Iterator<Item = (&str, &HostFunction)> {
        self.functions.iter().map(|(k, f)| (k.as_str(), f))
    }
}

impl FromIterator<(String, JsonValue)> for Bindings {
    fn from_iter<T: IntoIterator<Item = (String, JsonValue)>>(iter: T) -> Self {
        Bindings {
            vars: iter.into_iter().collect(),
            functions: IndexMap::new(),
        }
    }
}

/// Evaluation guardrails.
#[derive(Debug, Clone, Default)]
pub struct Options {
    /// Maximum wall-clock time for one evaluation. Exceeding it raises `D1012`.
    pub timeout: Option<Duration>,
    /// Maximum evaluation depth (default 50,000; 1,000 on WebAssembly). Exceeding it raises
    /// `D1011`.
    pub max_depth: Option<usize>,
}