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.
55pub struct Options {
56    pub(crate) macros: HashMap<String, Macro>,
57    pub(crate) functions: HashMap<String, Function>,
58    /// name -> (arity, closure)
59    pub(crate) natives: HashMap<String, (usize, NativeFn)>,
60    /// Current macro-expansion depth (transient parse state).
61    pub(crate) depth: AtomicUsize,
62    /// Whether the parser transparently converts legacy function objects
63    /// (`{type, property, stops, ...}`) to modern expressions before parsing.
64    /// On by default; see [`crate::convert`].
65    pub(crate) convert_legacy: bool,
66}
67
68impl Default for Options {
69    fn default() -> Options {
70        Options {
71            macros: HashMap::new(),
72            functions: HashMap::new(),
73            natives: HashMap::new(),
74            depth: AtomicUsize::new(0),
75            convert_legacy: true,
76        }
77    }
78}
79
80impl Options {
81    pub fn new() -> Options {
82        Options::default()
83    }
84
85    /// Enable or disable transparent conversion of legacy function objects
86    /// (on by default). When disabled, a bare JSON object is rejected as a
87    /// parse error rather than being treated as a legacy function.
88    pub fn convert_legacy(&mut self, enabled: bool) -> &mut Options {
89        self.convert_legacy = enabled;
90        self
91    }
92
93    /// Register a macro expanded at parse time.
94    pub fn macro_def(
95        &mut self,
96        name: impl Into<String>,
97        params: Vec<String>,
98        body: serde_json::Value,
99    ) -> &mut Options {
100        self.macros.insert(name.into(), Macro { params, body });
101        self
102    }
103
104    /// Register a function invoked at evaluation time (may recurse).
105    pub fn function(
106        &mut self,
107        name: impl Into<String>,
108        params: Vec<String>,
109        body: serde_json::Value,
110    ) -> &mut Options {
111        self.functions
112            .insert(name.into(), Function { params, body });
113        self
114    }
115
116    /// Register a native Rust function of the given arity. The closure receives
117    /// the evaluated argument values and the evaluation context.
118    pub fn native<F>(&mut self, name: impl Into<String>, arity: usize, f: F) -> &mut Options
119    where
120        F: Fn(&[Value], &EvaluationContext) -> Result<Value, EvalError> + Send + Sync + 'static,
121    {
122        self.natives.insert(name.into(), (arity, Arc::new(f)));
123        self
124    }
125}
126
127impl fmt::Debug for Options {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        f.debug_struct("Options")
130            .field("macros", &self.macros)
131            .field("functions", &self.functions)
132            .field("natives", &self.natives.keys().collect::<Vec<_>>())
133            .finish()
134    }
135}