jsonata_rs/
lib.rs

1#![cfg_attr(not(doctest), doc = include_str!("../README.md"))]
2use std::collections::HashMap;
3
4use bumpalo::Bump;
5
6mod datetime;
7mod errors;
8mod evaluator;
9mod parser;
10
11pub use errors::Error;
12pub use evaluator::functions::FunctionContext;
13pub use evaluator::value::{ArrayFlags, Value};
14
15use evaluator::{frame::Frame, functions::*, Evaluator};
16use parser::ast::Ast;
17
18pub type Result<T> = std::result::Result<T, Error>;
19
20pub struct JsonAta<'a> {
21    ast: Ast,
22    frame: Frame<'a>,
23    arena: &'a Bump,
24}
25
26impl<'a> JsonAta<'a> {
27    pub fn new(expr: &str, arena: &'a Bump) -> Result<JsonAta<'a>> {
28        Ok(Self {
29            ast: parser::parse(expr)?,
30            frame: Frame::new(),
31            arena,
32        })
33    }
34
35    pub fn ast(&self) -> &Ast {
36        &self.ast
37    }
38
39    pub fn assign_var(&self, name: &str, value: &'a Value<'a>) {
40        self.frame.bind(name, value)
41    }
42
43    pub fn register_function(
44        &self,
45        name: &str,
46        arity: usize,
47        implementation: fn(FunctionContext<'a, '_>, &[&'a Value<'a>]) -> Result<&'a Value<'a>>,
48    ) {
49        self.frame.bind(
50            name,
51            Value::nativefn(self.arena, name, arity, implementation),
52        );
53    }
54
55    fn json_value_to_value(&self, json_value: &serde_json::Value) -> &'a mut Value<'a> {
56        match json_value {
57            serde_json::Value::Null => Value::null(self.arena),
58            serde_json::Value::Bool(b) => self.arena.alloc(Value::Bool(*b)),
59            serde_json::Value::Number(n) => Value::number(self.arena, n.as_f64().unwrap()),
60            serde_json::Value::String(s) => Value::string(self.arena, s),
61
62            serde_json::Value::Array(a) => {
63                let array = Value::array_with_capacity(self.arena, a.len(), ArrayFlags::empty());
64                for v in a.iter() {
65                    array.push(self.json_value_to_value(v))
66                }
67
68                array
69            }
70            serde_json::Value::Object(o) => {
71                let object = Value::object_with_capacity(self.arena, o.len());
72                for (k, v) in o.iter() {
73                    object.insert(k, self.json_value_to_value(v));
74                }
75                object
76            }
77        }
78    }
79
80    pub fn evaluate(
81        &self,
82        input: Option<&str>,
83        bindings: Option<&HashMap<&str, &serde_json::Value>>,
84    ) -> Result<&'a Value<'a>> {
85        if let Some(bindings) = bindings {
86            for (key, json_value) in bindings.iter() {
87                let value = self.json_value_to_value(json_value);
88                self.assign_var(key, value);
89            }
90        };
91
92        self.evaluate_timeboxed(input, None, None)
93    }
94
95    pub fn evaluate_timeboxed(
96        &self,
97        input: Option<&str>,
98        max_depth: Option<usize>,
99        time_limit: Option<usize>,
100    ) -> Result<&'a Value<'a>> {
101        let input = match input {
102            Some(input) => {
103                let input_ast = parser::parse(input)?;
104                let evaluator = Evaluator::new(None, self.arena, None, None);
105                evaluator.evaluate(&input_ast, Value::undefined(), &Frame::new())?
106            }
107            None => Value::undefined(),
108        };
109
110        // If the input is an array, wrap it in an array so that it gets treated as a single input
111        let input = if input.is_array() {
112            Value::wrap_in_array(self.arena, input, ArrayFlags::WRAPPED)
113        } else {
114            input
115        };
116
117        macro_rules! bind_native {
118            ($name:literal, $arity:literal, $fn:ident) => {
119                self.frame
120                    .bind($name, Value::nativefn(&self.arena, $name, $arity, $fn));
121            };
122        }
123
124        self.frame.bind("$", input);
125        bind_native!("abs", 1, fn_abs);
126        bind_native!("append", 2, fn_append);
127        bind_native!("assert", 2, fn_assert);
128        bind_native!("base64decode", 1, fn_base64_decode);
129        bind_native!("base64encode", 1, fn_base64_encode);
130        bind_native!("boolean", 1, fn_boolean);
131        bind_native!("ceil", 1, fn_ceil);
132        bind_native!("contains", 2, fn_contains);
133        bind_native!("count", 1, fn_count);
134        bind_native!("distinct", 1, fn_distinct);
135        bind_native!("each", 2, fn_each);
136        bind_native!("error", 1, fn_error);
137        bind_native!("exists", 1, fn_exists);
138        bind_native!("fromMillis", 3, from_millis);
139        bind_native!("toMillis", 2, to_millis);
140        bind_native!("single", 2, single);
141        bind_native!("filter", 2, fn_filter);
142        bind_native!("floor", 1, fn_floor);
143        bind_native!("join", 2, fn_join);
144        bind_native!("keys", 1, fn_keys);
145        bind_native!("length", 1, fn_length);
146        bind_native!("lookup", 2, fn_lookup);
147        bind_native!("lowercase", 1, fn_lowercase);
148        bind_native!("map", 2, fn_map);
149        bind_native!("match", 2, fn_match);
150        bind_native!("max", 1, fn_max);
151        bind_native!("merge", 1, fn_merge);
152        bind_native!("min", 1, fn_min);
153        bind_native!("not", 1, fn_not);
154        bind_native!("now", 2, fn_now);
155        bind_native!("number", 1, fn_number);
156        bind_native!("pad", 2, fn_pad);
157        bind_native!("power", 2, fn_power);
158        bind_native!("random", 0, fn_random);
159        bind_native!("reduce", 3, fn_reduce);
160        bind_native!("replace", 4, fn_replace);
161        bind_native!("reverse", 1, fn_reverse);
162        bind_native!("round", 2, fn_round);
163        bind_native!("sort", 2, fn_sort);
164        bind_native!("split", 3, fn_split);
165        bind_native!("sqrt", 1, fn_sqrt);
166        bind_native!("string", 1, fn_string);
167        bind_native!("substring", 3, fn_substring);
168        bind_native!("substringBefore", 2, fn_substring_before);
169        bind_native!("substringAfter", 2, fn_substring_after);
170        bind_native!("sum", 1, fn_sum);
171        bind_native!("trim", 1, fn_trim);
172        bind_native!("uppercase", 1, fn_uppercase);
173        bind_native!("zip", 1, fn_zip);
174        bind_native!("millis", 0, fn_millis);
175        bind_native!("uuid", 0, fn_uuid);
176
177        let chain_ast = Some(parser::parse(
178            "function($f, $g) { function($x){ $g($f($x)) } }",
179        )?);
180        let evaluator = Evaluator::new(chain_ast, self.arena, max_depth, time_limit);
181        evaluator.evaluate(&self.ast, input, &self.frame)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use chrono::{DateTime, Datelike, Local, Offset};
188    use regress::Regex;
189
190    use bumpalo::collections::String as BumpString;
191
192    use super::*;
193
194    #[test]
195    fn register_function_simple() {
196        let arena = Bump::new();
197        let jsonata = JsonAta::new("$test()", &arena).unwrap();
198        jsonata.register_function("test", 0, |ctx, _| Ok(Value::number(ctx.arena, 1)));
199
200        let result = jsonata.evaluate(Some(r#"anything"#), None);
201
202        assert_eq!(result.unwrap(), Value::number(&arena, 1));
203    }
204
205    #[test]
206    fn register_function_override_now() {
207        let arena = Bump::new();
208        let jsonata = JsonAta::new("$now()", &arena).unwrap();
209        jsonata.register_function("now", 0, |ctx, _| {
210            Ok(Value::string(ctx.arena, "time for tea"))
211        });
212
213        let result = jsonata.evaluate(None, None);
214
215        assert_ne!(result.unwrap().as_str(), "time for tea");
216    }
217
218    #[test]
219    fn register_function_map_squareroot() {
220        let arena = Bump::new();
221        let jsonata = JsonAta::new("$map([1,4,9,16], $squareroot)", &arena).unwrap();
222        jsonata.register_function("squareroot", 1, |ctx, args| {
223            let num = &args[0];
224            Ok(Value::number(ctx.arena, (num.as_f64()).sqrt()))
225        });
226
227        let result = jsonata.evaluate(Some(r#"anything"#), None);
228
229        assert_eq!(
230            result
231                .unwrap()
232                .members()
233                .map(|v| v.as_f64())
234                .collect::<Vec<f64>>(),
235            vec![1.0, 2.0, 3.0, 4.0]
236        );
237    }
238
239    #[test]
240    fn register_function_filter_even() {
241        let arena = Bump::new();
242        let jsonata = JsonAta::new("$filter([1,4,9,16], $even)", &arena).unwrap();
243        jsonata.register_function("even", 1, |_ctx, args| {
244            let num = &args[0];
245            Ok(Value::bool((num.as_f64()) % 2.0 == 0.0))
246        });
247
248        let result = jsonata.evaluate(Some(r#"anything"#), None);
249
250        assert_eq!(
251            result
252                .unwrap()
253                .members()
254                .map(|v| v.as_f64())
255                .collect::<Vec<f64>>(),
256            vec![4.0, 16.0]
257        );
258    }
259
260    #[test]
261    fn evaluate_with_bindings_simple() {
262        let arena = Bump::new();
263        let jsonata = JsonAta::new("$a + $b", &arena).unwrap();
264
265        let a = &serde_json::Value::Number(serde_json::Number::from(1));
266        let b = &serde_json::Value::Number(serde_json::Number::from(2));
267
268        let mut bindings = HashMap::new();
269        bindings.insert("a", a);
270        bindings.insert("b", b);
271
272        let result = jsonata.evaluate(None, Some(&bindings));
273
274        assert_eq!(result.unwrap().as_f64(), 3.0);
275    }
276
277    #[test]
278    fn evaluate_with_bindings_nested() {
279        let arena = Bump::new();
280        let jsonata = JsonAta::new("$foo[0].a + $foo[1].b", &arena).unwrap();
281
282        let foo_string = r#"
283            [
284                {
285                    "a": 1
286                },
287                {
288                    "b": 2
289                }
290            ]
291        "#;
292
293        let foo: serde_json::Value = serde_json::from_str(foo_string).unwrap();
294
295        let mut bindings = HashMap::new();
296        bindings.insert("foo", &foo);
297
298        let result = jsonata.evaluate(None, Some(&bindings));
299
300        assert_eq!(result.unwrap().as_f64(), 3.0);
301    }
302
303    #[test]
304    fn evaluate_with_random() {
305        let arena = Bump::new();
306        let jsonata = JsonAta::new("$random()", &arena).unwrap();
307
308        let result = jsonata.evaluate(None, None).unwrap();
309
310        assert!(result.as_f64() >= 0.0);
311        assert!(result.as_f64() < 1.0);
312    }
313
314    #[test]
315    fn test_now_default_utc() {
316        let arena = Bump::new();
317        let jsonata = JsonAta::new("$now()", &arena).unwrap();
318        let result = jsonata.evaluate(None, None).unwrap();
319        let result_str = result.as_str();
320
321        println!("test_now_default_utc {}", result_str);
322
323        // Check for valid ISO 8601 format and ensure it's in UTC
324        let parsed_result = DateTime::parse_from_rfc3339(&result_str)
325            .expect("Should parse valid ISO 8601 timestamp");
326
327        assert_eq!(
328            parsed_result.offset().fix().local_minus_utc(),
329            0,
330            "Should be UTC"
331        );
332    }
333
334    #[test]
335    fn test_now_with_valid_timezone_and_format() {
336        let arena = Bump::new();
337        let jsonata = JsonAta::new(
338            "$now('[M01]/[D01]/[Y0001] [h#1]:[m01][P] [z]', '-0500')",
339            &arena,
340        )
341        .unwrap();
342        let result = jsonata.evaluate(None, None).unwrap();
343        let result_str = result.as_str();
344
345        println!("test_now_with_valid_timezone_and_format {}", result_str);
346
347        // Adjust the regex to allow both lowercase and uppercase AM/PM
348        let expected_format =
349            Regex::new(r"^\d{2}/\d{2}/\d{4} \d{1,2}:\d{2}(AM|PM|am|pm) GMT-05:00$").unwrap();
350
351        // Check if the pattern exists within the result_str
352        let is_match = expected_format.find_iter(&result_str).next().is_some();
353        assert!(
354            is_match,
355            "Expected custom formatted time with timezone, got: {}",
356            result_str
357        );
358    }
359
360    #[test]
361    fn test_now_with_valid_format_but_no_timezone() {
362        let arena = Bump::new();
363        let jsonata = JsonAta::new("$now('[M01]/[D01]/[Y0001] [h#1]:[m01][P]')", &arena).unwrap();
364        let result = jsonata.evaluate(None, None).unwrap();
365        let result_str = result.as_str();
366
367        let expected_format =
368            Regex::new(r"^\d{2}/\d{2}/\d{4} \d{1,2}:\d{2}(AM|PM|am|pm)$").unwrap();
369
370        // Allow both AM/PM and am/pm in the regex
371        let is_match = expected_format.find_iter(&result_str).next().is_some();
372        assert!(
373            is_match,
374            "Expected custom formatted time without timezone, got: {}",
375            result_str
376        );
377    }
378
379    #[test]
380    fn test_now_with_invalid_timezone() {
381        let arena = Bump::new();
382        let jsonata = JsonAta::new("$now('', 'invalid')", &arena).unwrap();
383        let result = jsonata.evaluate(None, None);
384
385        // Ensure that the evaluation results in an error
386        assert!(
387            result.is_err(),
388            "Expected a runtime error for invalid timezone"
389        );
390
391        // Get the error and check if it's the correct one
392        if let Err(err) = result {
393            // We expect a T0410ArgumentNotValid error
394            assert_eq!(
395                err.to_string(),
396                "T0410 @ 2: Argument 1 of function now does not match function signature"
397            );
398        }
399    }
400
401    #[test]
402    fn test_now_with_valid_timezone_but_no_format() {
403        let arena = Bump::new();
404        let jsonata = JsonAta::new("$now('', '-0500')", &arena).unwrap();
405        let result = jsonata.evaluate(None, None).unwrap();
406        let result_str = result.as_str();
407
408        println!("test_now_with_valid_timezone_but_no_format {}", result_str);
409
410        // Should return empty string for valid timezone but empty format
411        assert!(
412            result_str.is_empty(),
413            "Expected empty string for valid timezone but no format"
414        );
415    }
416
417    #[test]
418    fn test_now_with_too_many_arguments() {
419        let arena = Bump::new();
420
421        // Call $now() with more than two arguments (this should cause an error due to max_args! constraint)
422        let jsonata = JsonAta::new("$now('', '-0500', 'extra')", &arena).unwrap();
423        let result = jsonata.evaluate(None, None);
424
425        // Ensure that an error is returned for too many arguments
426        assert!(
427            result.is_err(),
428            "Expected an error due to too many arguments, but got a result"
429        );
430
431        if let Err(e) = result {
432            // You can also add an assertion to ensure the correct error type is returned
433            assert!(
434                matches!(e, Error::T0410ArgumentNotValid { .. }),
435                "Expected TooManyArguments error, but got: {:?}",
436                e
437            );
438        }
439    }
440
441    #[test]
442    fn test_now_with_edge_case_timezones() {
443        let arena = Bump::new();
444
445        // Extreme positive timezone
446        let jsonata = JsonAta::new("$now('[H01]:[m01] [z]', '+1440')", &arena).unwrap();
447        let result = jsonata.evaluate(None, None).unwrap();
448        let result_str = result.as_str();
449        println!("test_now_with_extreme_positive_timezone {:?}", result_str);
450        assert!(result_str.contains("GMT+14:40"), "Expected GMT+14:40");
451
452        // Edge case: minimal valid timezone
453        let jsonata_minimal = JsonAta::new("$now('[H01]:[m01] [z]', '-0000')", &arena).unwrap();
454        let result_minimal = jsonata_minimal.evaluate(None, None).unwrap();
455        let result_str_minimal = result_minimal.as_str();
456        println!("test_now_with_minimal_timezone {:?}", result_str_minimal);
457
458        // Corrected the assertion to check for "GMT+00:00"
459        assert!(
460            result_str_minimal.contains("GMT+00:00"),
461            "Expected GMT+00:00"
462        );
463    }
464
465    #[test]
466    fn test_custom_format_with_components() {
467        let arena = Bump::new();
468
469        // 12-hour format with AM/PM and timezone
470        let jsonata = JsonAta::new(
471            "$now('[M01]/[D01]/[Y0001] [h#1]:[m01][P] [z]', '-0500')",
472            &arena,
473        )
474        .unwrap();
475        let result = jsonata.evaluate(None, None).unwrap();
476        let result_str = result.as_str();
477
478        println!("Formatted date: {}", result_str);
479
480        // Create the regex with regress::Regex
481        let expected_format =
482            Regex::new(r"^\d{2}/\d{2}/\d{4} \d{1,2}:\d{2}(am|pm|AM|PM) GMT-05:00$").unwrap();
483
484        // Check if the pattern exists within result_str using find_iter
485        let is_match = expected_format.find_iter(&result_str).next().is_some();
486        assert!(
487            is_match,
488            "Expected 12-hour format with timezone, got: {}",
489            result_str
490        );
491    }
492
493    #[test]
494    fn test_now_with_invalid_timezone_should_fail() {
495        let arena = Bump::new();
496        let jsonata = JsonAta::new("$now('', 'invalid')", &arena).unwrap();
497        let result = jsonata.evaluate(None, None);
498
499        assert!(
500            result.is_err(),
501            "Expected a runtime error for invalid timezone"
502        );
503
504        let error = result.unwrap_err();
505        assert_eq!(
506            error.to_string(),
507            "T0410 @ 2: Argument 1 of function now does not match function signature"
508        );
509    }
510
511    #[test]
512    fn test_now_invalid_timezone_with_format() {
513        let arena = Bump::new();
514        let jsonata = JsonAta::new("$now('[h]:[m]:[s]', 'xx')", &arena).unwrap();
515        let result = jsonata.evaluate(None, None);
516
517        assert!(
518            result.is_err(),
519            "Expected a runtime error for invalid timezone"
520        );
521        if let Err(err) = result {
522            assert_eq!(
523                err.to_string(),
524                "T0410 @ 2: Argument 1 of function now does not match function signature"
525            );
526        }
527    }
528
529    #[test]
530    fn test_now_invalid_format_with_timezone() {
531        let arena = Bump::new();
532        let jsonata = JsonAta::new("$now('[h01]:[m01]:[s01]', 'xx')", &arena).unwrap();
533        let result = jsonata.evaluate(None, None);
534
535        assert!(
536            result.is_err(),
537            "Expected a runtime error for invalid format"
538        );
539        if let Err(err) = result {
540            assert_eq!(
541                err.to_string(),
542                "T0410 @ 2: Argument 1 of function now does not match function signature"
543            );
544        }
545    }
546
547    #[test]
548    fn test_from_millis_with_custom_format() {
549        let arena = Bump::new();
550
551        // Create an instance of JsonAta with a custom format for the date
552        let jsonata = JsonAta::new(
553            "$fromMillis(1726148700000, '[M01]/[D01]/[Y0001] [H01]:[m01]:[s01] [z]')",
554            &arena,
555        )
556        .unwrap();
557
558        // Evaluate the expression and unwrap the result
559        let result = jsonata.evaluate(None, None).unwrap();
560        let result_str = result.as_str();
561
562        println!("Formatted date: {}", result_str);
563
564        // Define the expected format using regress::Regex
565        let expected_format =
566            Regex::new(r"^\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2} GMT\+\d{2}:\d{2}$").unwrap();
567
568        // Simulate `is_match` by checking if there's at least one match in the string
569        let is_match = expected_format.find_iter(&result_str).next().is_some();
570        assert!(
571            is_match,
572            "Expected custom formatted date with timezone, got: {}",
573            result_str
574        );
575    }
576
577    #[test]
578    fn test_to_millis_with_custom_format() {
579        let arena = Bump::new();
580
581        // Create an instance of JsonAta with a custom format to convert to millis
582        let jsonata = JsonAta::new(
583            "$toMillis('13/09/2024 13:45:00', '[D01]/[M01]/[Y0001] [H01]:[m01]:[s01]')",
584            &arena,
585        )
586        .unwrap();
587
588        // Evaluate the expression and unwrap the result
589        let result = jsonata.evaluate(None, None);
590
591        match result {
592            Ok(value) => {
593                if value.is_number() {
594                    let millis = value.as_f64();
595
596                    // Check if the milliseconds value matches the expected timestamp
597                    assert_eq!(
598                        millis, 1726235100000.0,
599                        "Expected milliseconds for the given date"
600                    );
601                } else {
602                    println!("Result is not a number: {:?}", value);
603                    panic!("Expected a number, but got something else.");
604                }
605            }
606            Err(err) => {
607                println!("Evaluation error: {:?}", err);
608                panic!("Failed to evaluate the expression.");
609            }
610        }
611    }
612
613    #[test]
614    fn evaluate_with_reduce() {
615        let arena = Bump::new(); // Initialize the memory arena
616        let jsonata = JsonAta::new(
617            "$reduce([1..5], function($i, $j){$i * $j})", // Example expression
618            &arena,
619        )
620        .unwrap();
621
622        let result = jsonata.evaluate(None, None).unwrap(); // Evaluate the expression
623
624        // Assert that the result is the expected product of 1 * 2 * 3 * 4 * 5 = 120
625        assert_eq!(result.as_f64(), 120.0);
626    }
627
628    #[test]
629    fn evaluate_with_reduce_sum() {
630        let arena = Bump::new();
631        let jsonata = JsonAta::new(
632            "$reduce([1..5], function($i, $j){$i + $j})", // Adding 1 to 5
633            &arena,
634        )
635        .unwrap();
636
637        let result = jsonata.evaluate(None, None).unwrap();
638
639        // Assert that the result is 15 (1 + 2 + 3 + 4 + 5 = 15)
640        assert_eq!(result.as_f64(), 15.0);
641    }
642
643    #[test]
644    fn evaluate_with_reduce_custom_initial_value() {
645        let arena = Bump::new();
646        let jsonata = JsonAta::new(
647            "$reduce([1..5], function($i, $j){$i * $j}, 10)", // Multiply with initial value 10
648            &arena,
649        )
650        .unwrap();
651
652        let result = jsonata.evaluate(None, None).unwrap();
653
654        // Assert that the result is 1200 (10 * 1 * 2 * 3 * 4 * 5 = 1200)
655        assert_eq!(result.as_f64(), 1200.0);
656    }
657
658    #[test]
659    fn evaluate_with_reduce_empty_array() {
660        let arena = Bump::new();
661        let jsonata = JsonAta::new(
662            "$reduce([], function($i, $j){$i + $j})", // Reducing an empty array
663            &arena,
664        )
665        .unwrap();
666
667        let result = jsonata.evaluate(None, None).unwrap();
668
669        // Since the array is empty, the result should be `undefined`
670        assert!(result.is_undefined());
671    }
672
673    #[test]
674    fn evaluate_with_reduce_single_element() {
675        let arena = Bump::new();
676        let jsonata = JsonAta::new(
677            "$reduce([42], function($i, $j){$i + $j})", // Single element array
678            &arena,
679        )
680        .unwrap();
681
682        let result = jsonata.evaluate(None, None).unwrap();
683
684        // Assert that the result is 42 (only one element, so it returns that element)
685        assert_eq!(result.as_f64(), 42.0);
686    }
687
688    #[test]
689    fn evaluate_with_reduce_subtraction() {
690        let arena = Bump::new();
691        let jsonata = JsonAta::new(
692            "$reduce([10, 3, 2], function($i, $j){$i - $j})", // Subtracting elements
693            &arena,
694        )
695        .unwrap();
696
697        let result = jsonata.evaluate(None, None).unwrap();
698
699        // Assert that the result is 5 (10 - 3 - 2 = 5)
700        assert_eq!(result.as_f64(), 5.0);
701    }
702
703    #[test]
704    fn evaluate_with_reduce_initial_value_greater() {
705        let arena = Bump::new();
706        let jsonata = JsonAta::new(
707            "$reduce([1..3], function($i, $j){$i - $j}, 10)", // Initial value is greater than array elements
708            &arena,
709        )
710        .unwrap();
711
712        let result = jsonata.evaluate(None, None).unwrap();
713
714        // Assert that the result is 4 (10 - 1 - 2 - 3 = 4)
715        assert_eq!(result.as_f64(), 4.0);
716    }
717
718    #[test]
719    fn test_match_regex_with_jsonata() {
720        let arena = Bump::new();
721
722        // Test case with a valid postal code
723        let jsonata = JsonAta::new(r#"$match("123456789", /^[0-9]{9}$/)"#, &arena).unwrap();
724        let result = jsonata.evaluate(None, None).unwrap();
725
726        // Expected output: an array with a single match object for "123456789"
727        let match_value: &Value = arena.alloc(Value::string(&arena, "123456789"));
728        let index_value: &Value = arena.alloc(Value::number(&arena, 0.0));
729        let groups_array: &Value = &*arena.alloc(Value::Array(
730            bumpalo::collections::Vec::new_in(&arena),
731            ArrayFlags::empty(),
732        ));
733
734        let mut match_obj = hashbrown::HashMap::with_capacity_in(3, &arena);
735        match_obj.insert(BumpString::from_str_in("match", &arena), match_value);
736        match_obj.insert(BumpString::from_str_in("index", &arena), index_value);
737        match_obj.insert(BumpString::from_str_in("groups", &arena), groups_array);
738
739        let expected_match: &Value = &*arena.alloc(Value::Object(match_obj));
740
741        assert_eq!(
742            result,
743            &*arena.alloc(Value::Array(
744                bumpalo::collections::Vec::from_iter_in([expected_match], &arena),
745                ArrayFlags::empty()
746            ))
747        );
748
749        // Test case with an invalid postal code
750        let jsonata_invalid =
751            JsonAta::new(r#"$match("12345-6789", /^[0-9]{9}$/)"#, &arena).unwrap();
752        let result_invalid = jsonata_invalid.evaluate(None, None).unwrap();
753
754        // Expected output for invalid input: an empty array
755        let empty_array: &Value = &*arena.alloc(Value::Array(
756            bumpalo::collections::Vec::new_in(&arena), // Empty array for no matches
757            ArrayFlags::empty(),
758        ));
759        assert_eq!(result_invalid, empty_array);
760    }
761
762    #[test]
763    fn evaluate_expect_type_errors() {
764        for expr in [
765            "$fromMillis('foo')",
766            "$fromMillis(1, 1)",
767            "$fromMillis(1, '', 1)",
768            "$toMillis(1)",
769            "$toMillis('1970-01-01T00:00:00.000Z', 1)",
770        ] {
771            let arena = Bump::new();
772            let jsonata = JsonAta::new(expr, &arena).unwrap();
773            let err = jsonata.evaluate(None, None).unwrap_err();
774
775            assert_eq!(err.code(), "T0410", "Expected type error from {expr}");
776        }
777    }
778
779    #[test]
780    fn evaluate_millis_returns_number() {
781        let arena = Bump::new();
782        let jsonata = JsonAta::new("$millis()", &arena).unwrap();
783        let result = jsonata.evaluate(None, None).unwrap();
784
785        assert!(result.is_number());
786    }
787
788    #[test]
789    fn test_from_millis_formats_date() {
790        // Initialize the arena (memory pool) for JSONata
791        let arena = Bump::new();
792
793        // Define the JSONata expression for formatting the date
794        let jsonata = JsonAta::new("$fromMillis($millis(),'[Y01][M01][D01]')", &arena).unwrap();
795
796        // Evaluate the expression
797        let result = jsonata.evaluate(None, None).unwrap();
798
799        // Dynamically compute the expected result
800        let now = Local::now();
801        let expected = format!(
802            "{:02}{:02}{:02}",
803            now.year() % 100, // Last two digits of the year
804            now.month(),
805            now.day()
806        );
807
808        // Store the result of `to_string` in a variable to ensure it lives long enough
809        let result_string = result.to_string();
810        let actual = result_string.trim_matches('"'); // Trim quotes if present
811
812        // Verify the result matches the expected value
813        assert_eq!(actual, expected);
814    }
815}