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}