Skip to main content

jpx_core/
runtime.rs

1//! JMESPath runtime for compiling and evaluating expressions.
2
3use std::collections::HashMap;
4
5use crate::Expression;
6use crate::JmespathError;
7use crate::functions::*;
8use crate::parse;
9use crate::registry::{Category, FunctionRegistry};
10
11/// Compiles JMESPath expressions and manages registered functions.
12pub struct Runtime {
13    functions: HashMap<String, Box<dyn Function>>,
14}
15
16impl Default for Runtime {
17    fn default() -> Self {
18        Runtime {
19            functions: HashMap::with_capacity(26),
20        }
21    }
22}
23
24impl Runtime {
25    /// Creates a new Runtime with no functions registered.
26    pub fn new() -> Runtime {
27        Default::default()
28    }
29
30    /// Creates a strict-mode runtime with only the 26 spec functions.
31    pub fn strict() -> Runtime {
32        let mut rt = Runtime::new();
33        rt.register_builtin_functions();
34        rt
35    }
36
37    /// Creates a RuntimeBuilder for selective function registration.
38    pub fn builder() -> RuntimeBuilder {
39        RuntimeBuilder::new()
40    }
41
42    /// Creates a new JMESPath expression from an expression string.
43    #[inline]
44    pub fn compile<'a>(&'a self, expression: &str) -> Result<Expression<'a>, JmespathError> {
45        parse(expression).map(|ast| Expression::new(expression, ast, self))
46    }
47
48    /// Adds a new function to the runtime.
49    #[inline]
50    pub fn register_function(&mut self, name: &str, f: Box<dyn Function>) {
51        self.functions.insert(name.to_owned(), f);
52    }
53
54    /// Removes a function from the runtime.
55    pub fn deregister_function(&mut self, name: &str) -> Option<Box<dyn Function>> {
56        self.functions.remove(name)
57    }
58
59    /// Gets a function by name from the runtime.
60    #[inline]
61    pub fn get_function<'a>(&'a self, name: &str) -> Option<&'a dyn Function> {
62        self.functions.get(name).map(AsRef::as_ref)
63    }
64
65    /// Returns an iterator over all registered function names.
66    pub fn function_names(&self) -> impl Iterator<Item = &str> {
67        self.functions.keys().map(|s| s.as_str())
68    }
69
70    /// Registers all 26 built-in JMESPath functions.
71    pub fn register_builtin_functions(&mut self) {
72        self.register_function("abs", Box::new(AbsFn::new()));
73        self.register_function("avg", Box::new(AvgFn::new()));
74        self.register_function("ceil", Box::new(CeilFn::new()));
75        self.register_function("contains", Box::new(ContainsFn::new()));
76        self.register_function("ends_with", Box::new(EndsWithFn::new()));
77        self.register_function("floor", Box::new(FloorFn::new()));
78        self.register_function("join", Box::new(JoinFn::new()));
79        self.register_function("keys", Box::new(KeysFn::new()));
80        self.register_function("length", Box::new(LengthFn::new()));
81        self.register_function("map", Box::new(MapFn::new()));
82        self.register_function("min", Box::new(MinFn::new()));
83        self.register_function("max", Box::new(MaxFn::new()));
84        self.register_function("max_by", Box::new(MaxByFn::new()));
85        self.register_function("min_by", Box::new(MinByFn::new()));
86        self.register_function("merge", Box::new(MergeFn::new()));
87        self.register_function("not_null", Box::new(NotNullFn::new()));
88        self.register_function("reverse", Box::new(ReverseFn::new()));
89        self.register_function("sort", Box::new(SortFn::new()));
90        self.register_function("sort_by", Box::new(SortByFn::new()));
91        self.register_function("starts_with", Box::new(StartsWithFn::new()));
92        self.register_function("sum", Box::new(SumFn::new()));
93        self.register_function("to_array", Box::new(ToArrayFn::new()));
94        self.register_function("to_number", Box::new(ToNumberFn::new()));
95        self.register_function("to_string", Box::new(ToStringFn::new()));
96        self.register_function("type", Box::new(TypeFn::new()));
97        self.register_function("values", Box::new(ValuesFn::new()));
98    }
99}
100
101/// Builder for creating a Runtime with selective function registration.
102///
103/// # Example
104///
105/// ```
106/// use jpx_core::Runtime;
107/// use jpx_core::Category;
108///
109/// let rt = Runtime::builder()
110///     .with_standard()
111///     .with_category(Category::String)
112///     .with_category(Category::Math)
113///     .build();
114/// ```
115pub struct RuntimeBuilder {
116    registry: FunctionRegistry,
117    include_standard: bool,
118}
119
120impl RuntimeBuilder {
121    fn new() -> Self {
122        RuntimeBuilder {
123            registry: FunctionRegistry::new(),
124            include_standard: false,
125        }
126    }
127
128    /// Includes the 26 standard JMESPath functions.
129    pub fn with_standard(mut self) -> Self {
130        self.include_standard = true;
131        self
132    }
133
134    /// Includes all functions from a specific category.
135    pub fn with_category(mut self, category: Category) -> Self {
136        self.registry.register_category(category);
137        self
138    }
139
140    /// Includes all available extension functions.
141    pub fn with_all_extensions(mut self) -> Self {
142        self.registry.register_all();
143        self
144    }
145
146    /// Disables a specific function.
147    pub fn without_function(mut self, name: &str) -> Self {
148        self.registry.disable_function(name);
149        self
150    }
151
152    /// Builds the runtime with the configured functions.
153    pub fn build(self) -> Runtime {
154        let mut rt = Runtime::new();
155        if self.include_standard {
156            rt.register_builtin_functions();
157        }
158        self.registry.apply(&mut rt);
159        rt
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use serde_json::json;
167
168    #[test]
169    fn new_runtime_has_no_functions() {
170        let rt = Runtime::new();
171        assert!(rt.get_function("abs").is_none());
172        assert_eq!(rt.function_names().count(), 0);
173    }
174
175    #[test]
176    fn strict_runtime_has_26_builtins() {
177        let rt = Runtime::strict();
178        assert_eq!(rt.function_names().count(), 26);
179        assert!(rt.get_function("abs").is_some());
180        assert!(rt.get_function("length").is_some());
181        assert!(rt.get_function("sort").is_some());
182    }
183
184    #[test]
185    fn register_and_get_function() {
186        let mut rt = Runtime::new();
187        rt.register_function("abs", Box::new(AbsFn::new()));
188        assert!(rt.get_function("abs").is_some());
189        assert!(rt.get_function("nonexistent").is_none());
190    }
191
192    #[test]
193    fn deregister_function() {
194        let mut rt = Runtime::strict();
195        assert!(rt.get_function("abs").is_some());
196        let removed = rt.deregister_function("abs");
197        assert!(removed.is_some());
198        assert!(rt.get_function("abs").is_none());
199        assert_eq!(rt.function_names().count(), 25);
200    }
201
202    #[test]
203    fn deregister_nonexistent_returns_none() {
204        let mut rt = Runtime::new();
205        assert!(rt.deregister_function("nope").is_none());
206    }
207
208    #[test]
209    fn builder_with_standard() {
210        let rt = Runtime::builder().with_standard().build();
211        assert_eq!(rt.function_names().count(), 26);
212    }
213
214    #[test]
215    #[cfg(feature = "extensions")]
216    fn builder_with_category() {
217        let rt = Runtime::builder()
218            .with_standard()
219            .with_category(Category::String)
220            .build();
221        assert!(rt.function_names().count() > 26);
222        assert!(rt.get_function("lower").is_some());
223    }
224
225    #[test]
226    #[cfg(feature = "extensions")]
227    fn builder_with_all_extensions() {
228        let rt = Runtime::builder()
229            .with_standard()
230            .with_all_extensions()
231            .build();
232        assert!(rt.function_names().count() > 26);
233    }
234
235    #[test]
236    #[cfg(feature = "extensions")]
237    fn builder_without_function() {
238        let rt = Runtime::builder()
239            .with_standard()
240            .with_all_extensions()
241            .without_function("lower")
242            .build();
243        assert!(rt.get_function("lower").is_none());
244        assert!(rt.get_function("upper").is_some());
245    }
246
247    #[test]
248    fn compile_with_runtime() {
249        let rt = Runtime::strict();
250        let expr = rt.compile("length(@)").unwrap();
251        let result = expr.search(&json!([1, 2, 3])).unwrap();
252        assert_eq!(result, json!(3));
253    }
254
255    #[test]
256    fn unknown_function_compile_succeeds_search_fails() {
257        let rt = Runtime::new();
258        let expr = rt.compile("nonexistent(@)").unwrap();
259        let result = expr.search(&json!(null));
260        assert!(result.is_err());
261    }
262
263    #[test]
264    fn function_names_iterator() {
265        let rt = Runtime::strict();
266        let names: Vec<&str> = rt.function_names().collect();
267        assert!(names.contains(&"abs"));
268        assert!(names.contains(&"length"));
269        assert!(names.contains(&"values"));
270    }
271}