Skip to main content

maplibre_expr/
ext.rs

1//! Optional user extensions: macros, expression functions, and native (Rust)
2//! functions plugged into the parser and runtime.
3//!
4//! - A **macro** ([`Options::macro_def`]) is expanded at parse time into
5//!   `["let", ...]` binding its parameters to the call arguments — zero runtime
6//!   cost, but no recursion (a depth limit guards against cycles).
7//! - A **function** ([`Options::function`]) is left as a call in the tree and
8//!   invoked at evaluation time, so it may recurse (bounded by a call-depth
9//!   limit).
10//! - A **native function** ([`Options::native`]) is a Rust closure invoked with
11//!   the evaluated argument values (and the context), returning a value
12//!   dynamically.
13//!
14//! All are provided via [`Options`], passed to [`parse_with`](crate::parse_with)
15//! and [`evaluate_with`](crate::evaluate_with). [`Options`] is `Send + Sync`
16//! (native closures must be too), so a registry can be shared across threads.
17
18use std::collections::HashMap;
19use std::fmt;
20use std::sync::atomic::AtomicUsize;
21use std::sync::Arc;
22
23use crate::context::EvaluationContext;
24use crate::error::EvalError;
25use crate::value::Value;
26
27/// Maximum macro-expansion depth before assuming a recursive macro.
28pub(crate) const MAX_MACRO_DEPTH: usize = 64;
29/// Maximum user-function call depth before erroring. Kept conservative so deep
30/// recursion errors cleanly rather than overflowing the native stack.
31pub(crate) const MAX_CALL_DEPTH: usize = 64;
32
33/// A native function: called with the evaluated arguments and the context.
34pub type NativeFn =
35    Arc<dyn Fn(&[Value], &EvaluationContext) -> Result<Value, EvalError> + Send + Sync>;
36
37/// A parse-time macro: `body` is expanded with `params` bound to the call
38/// arguments (as a `let`). `body` is raw JSON in the expression grammar.
39#[derive(Debug, Clone)]
40pub struct Macro {
41    pub params: Vec<String>,
42    pub body: serde_json::Value,
43}
44
45/// An eval-time function: `body` (raw JSON) is evaluated with `params` bound to
46/// the argument values. May reference itself or other functions (recursion is
47/// bounded at runtime).
48#[derive(Debug, Clone)]
49pub struct Function {
50    pub params: Vec<String>,
51    pub body: serde_json::Value,
52}
53
54/// Parser/runtime extension registry.
55#[derive(Default)]
56pub struct Options {
57    pub(crate) macros: HashMap<String, Macro>,
58    pub(crate) functions: HashMap<String, Function>,
59    /// name -> (arity, closure)
60    pub(crate) natives: HashMap<String, (usize, NativeFn)>,
61    /// Current macro-expansion depth (transient parse state).
62    pub(crate) depth: AtomicUsize,
63}
64
65impl Options {
66    pub fn new() -> Options {
67        Options::default()
68    }
69
70    /// Register a macro expanded at parse time.
71    pub fn macro_def(
72        &mut self,
73        name: impl Into<String>,
74        params: Vec<String>,
75        body: serde_json::Value,
76    ) -> &mut Options {
77        self.macros.insert(name.into(), Macro { params, body });
78        self
79    }
80
81    /// Register a function invoked at evaluation time (may recurse).
82    pub fn function(
83        &mut self,
84        name: impl Into<String>,
85        params: Vec<String>,
86        body: serde_json::Value,
87    ) -> &mut Options {
88        self.functions
89            .insert(name.into(), Function { params, body });
90        self
91    }
92
93    /// Register a native Rust function of the given arity. The closure receives
94    /// the evaluated argument values and the evaluation context.
95    pub fn native<F>(&mut self, name: impl Into<String>, arity: usize, f: F) -> &mut Options
96    where
97        F: Fn(&[Value], &EvaluationContext) -> Result<Value, EvalError> + Send + Sync + 'static,
98    {
99        self.natives.insert(name.into(), (arity, Arc::new(f)));
100        self
101    }
102}
103
104impl fmt::Debug for Options {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        f.debug_struct("Options")
107            .field("macros", &self.macros)
108            .field("functions", &self.functions)
109            .field("natives", &self.natives.keys().collect::<Vec<_>>())
110            .finish()
111    }
112}