github_actions_expressions/
call.rs

1//! Representation of function calls in GitHub Actions expressions.
2
3use crate::{Evaluation, SpannedExpr};
4
5/// Represents a function in a GitHub Actions expression.
6///
7/// Function names are case-insensitive.
8#[derive(Debug)]
9pub struct Function<'src>(pub(crate) &'src str);
10
11impl PartialEq for Function<'_> {
12    fn eq(&self, other: &Self) -> bool {
13        self.0.eq_ignore_ascii_case(other.0)
14    }
15}
16impl PartialEq<str> for Function<'_> {
17    fn eq(&self, other: &str) -> bool {
18        self.0.eq_ignore_ascii_case(other)
19    }
20}
21
22/// Represents a function call in a GitHub Actions expression.
23#[derive(Debug, PartialEq)]
24pub struct Call<'src> {
25    /// The function name, e.g. `foo` in `foo()`.
26    pub func: Function<'src>,
27    /// The function's arguments.
28    pub args: Vec<SpannedExpr<'src>>,
29}
30
31impl<'src> Call<'src> {
32    /// Performs constant evaluation of a GitHub Actions expression
33    /// function call.
34    pub(crate) fn consteval(&self) -> Option<Evaluation> {
35        let args = self
36            .args
37            .iter()
38            .map(|arg| arg.consteval())
39            .collect::<Option<Vec<Evaluation>>>()?;
40
41        match &self.func {
42            f if f == "format" => Self::consteval_format(&args),
43            f if f == "contains" => Self::consteval_contains(&args),
44            f if f == "startsWith" => Self::consteval_startswith(&args),
45            f if f == "endsWith" => Self::consteval_endswith(&args),
46            f if f == "toJSON" => Self::consteval_tojson(&args),
47            f if f == "fromJSON" => Self::consteval_fromjson(&args),
48            f if f == "join" => Self::consteval_join(&args),
49            _ => None,
50        }
51    }
52
53    /// Constant-evaluates a `format(fmtspec, args...)` call.
54    ///
55    /// See: <https://github.com/actions/languageservices/blob/1f3436c3cacc0f99d5d79e7120a5a9270cf13a72/expressions/src/funcs/format.ts>
56    fn consteval_format(args: &[Evaluation]) -> Option<Evaluation> {
57        if args.is_empty() {
58            return None;
59        }
60
61        let template = args[0].sema().to_string();
62        let mut result = String::new();
63        let mut index = 0;
64
65        while index < template.len() {
66            let lbrace = template[index..].find('{').map(|pos| index + pos);
67            let rbrace = template[index..].find('}').map(|pos| index + pos);
68
69            // Left brace
70            #[allow(clippy::unwrap_used)]
71            if let Some(lbrace_pos) = lbrace
72                && (rbrace.is_none() || rbrace.unwrap() > lbrace_pos)
73            {
74                // Escaped left brace
75                if template.as_bytes().get(lbrace_pos + 1) == Some(&b'{') {
76                    result.push_str(&template[index..=lbrace_pos]);
77                    index = lbrace_pos + 2;
78                    continue;
79                }
80
81                // Left brace, number, optional format specifiers, right brace
82                if let Some(rbrace_pos) = rbrace
83                    && rbrace_pos > lbrace_pos + 1
84                    && let Some(arg_index) = Self::read_arg_index(&template, lbrace_pos + 1)
85                {
86                    // Check parameter count
87                    if 1 + arg_index > args.len() - 1 {
88                        // Invalid format string - too few arguments
89                        return None;
90                    }
91
92                    // Append the portion before the left brace
93                    if lbrace_pos > index {
94                        result.push_str(&template[index..lbrace_pos]);
95                    }
96
97                    // Append the arg
98                    result.push_str(&args[1 + arg_index].sema().to_string());
99                    index = rbrace_pos + 1;
100                    continue;
101                }
102
103                // Invalid format string
104                return None;
105            }
106
107            // Right brace
108            if let Some(rbrace_pos) = rbrace {
109                #[allow(clippy::unwrap_used)]
110                if lbrace.is_none() || lbrace.unwrap() > rbrace_pos {
111                    // Escaped right brace
112                    if template.as_bytes().get(rbrace_pos + 1) == Some(&b'}') {
113                        result.push_str(&template[index..=rbrace_pos]);
114                        index = rbrace_pos + 2;
115                    } else {
116                        // Invalid format string
117                        return None;
118                    }
119                }
120            } else {
121                // Last segment
122                result.push_str(&template[index..]);
123                break;
124            }
125        }
126
127        Some(Evaluation::String(result))
128    }
129
130    /// Helper function to read argument index from format string.
131    fn read_arg_index(string: &str, start_index: usize) -> Option<usize> {
132        let mut length = 0;
133        let chars: Vec<char> = string.chars().collect();
134
135        // Count the number of digits
136        while start_index + length < chars.len() {
137            let next_char = chars[start_index + length];
138            if next_char.is_ascii_digit() {
139                length += 1;
140            } else {
141                break;
142            }
143        }
144
145        // Validate at least one digit
146        if length < 1 {
147            return None;
148        }
149
150        // Parse the number
151        let number_str: String = chars[start_index..start_index + length].iter().collect();
152        number_str.parse::<usize>().ok()
153    }
154
155    /// Constant-evaluates a `contains(haystack, needle)` call.
156    ///
157    /// See: <https://github.com/actions/languageservices/blob/1f3436c3cacc0f99d5d79e7120a5a9270cf13a72/expressions/src/funcs/contains.ts>
158    fn consteval_contains(args: &[Evaluation]) -> Option<Evaluation> {
159        if args.len() != 2 {
160            return None;
161        }
162
163        let search = &args[0];
164        let item = &args[1];
165
166        match search {
167            // For primitive types (strings, numbers, booleans, null), do case-insensitive string search
168            Evaluation::String(_)
169            | Evaluation::Number(_)
170            | Evaluation::Boolean(_)
171            | Evaluation::Null => {
172                let search_str = search.sema().to_string().to_lowercase();
173                let item_str = item.sema().to_string().to_lowercase();
174                Some(Evaluation::Boolean(search_str.contains(&item_str)))
175            }
176            // For arrays, check if any element equals the item
177            Evaluation::Array(arr) => {
178                if arr.iter().any(|element| element.sema() == item.sema()) {
179                    Some(Evaluation::Boolean(true))
180                } else {
181                    Some(Evaluation::Boolean(false))
182                }
183            }
184            // `contains(object, ...)` is not defined in the reference implementation
185            Evaluation::Object(_) => None,
186        }
187    }
188
189    /// Constant-evaluates a `startsWith(string, prefix)` call.
190    ///
191    /// See: <https://github.com/actions/languageservices/blob/1f3436c3cacc0f99d5d79e7120a5a9270cf13a72/expressions/src/funcs/startswith.ts>
192    fn consteval_startswith(args: &[Evaluation]) -> Option<Evaluation> {
193        if args.len() != 2 {
194            return None;
195        }
196
197        let search_string = &args[0];
198        let search_value = &args[1];
199
200        // Both arguments must be primitive types (not arrays or dictionaries)
201        match (search_string, search_value) {
202            (
203                Evaluation::String(_)
204                | Evaluation::Number(_)
205                | Evaluation::Boolean(_)
206                | Evaluation::Null,
207                Evaluation::String(_)
208                | Evaluation::Number(_)
209                | Evaluation::Boolean(_)
210                | Evaluation::Null,
211            ) => {
212                // Case-insensitive comparison
213                let string_str = search_string.sema().to_string().to_lowercase();
214                let prefix_str = search_value.sema().to_string().to_lowercase();
215                Some(Evaluation::Boolean(string_str.starts_with(&prefix_str)))
216            }
217            // If either argument is not primitive (array or dictionary), return false
218            _ => Some(Evaluation::Boolean(false)),
219        }
220    }
221
222    /// Constant-evaluates an `endsWith(string, suffix)` call.
223    ///
224    /// See: <https://github.com/actions/languageservices/blob/1f3436c3cacc0f99d5d79e7120a5a9270cf13a72/expressions/src/funcs/endswith.ts>
225    fn consteval_endswith(args: &[Evaluation]) -> Option<Evaluation> {
226        if args.len() != 2 {
227            return None;
228        }
229
230        let search_string = &args[0];
231        let search_value = &args[1];
232
233        // Both arguments must be primitive types (not arrays or dictionaries)
234        match (search_string, search_value) {
235            (
236                Evaluation::String(_)
237                | Evaluation::Number(_)
238                | Evaluation::Boolean(_)
239                | Evaluation::Null,
240                Evaluation::String(_)
241                | Evaluation::Number(_)
242                | Evaluation::Boolean(_)
243                | Evaluation::Null,
244            ) => {
245                // Case-insensitive comparison
246                let string_str = search_string.sema().to_string().to_lowercase();
247                let suffix_str = search_value.sema().to_string().to_lowercase();
248                Some(Evaluation::Boolean(string_str.ends_with(&suffix_str)))
249            }
250            // If either argument is not primitive (array or dictionary), return false
251            _ => Some(Evaluation::Boolean(false)),
252        }
253    }
254
255    /// Constant-evaluates a `toJSON(value)` call.
256    ///
257    /// See: <https://github.com/actions/languageservices/blob/1f3436c3cacc0f99d5d79e7120a5a9270cf13a72/expressions/src/funcs/tojson.ts>
258    fn consteval_tojson(args: &[Evaluation]) -> Option<Evaluation> {
259        if args.len() != 1 {
260            return None;
261        }
262
263        let value = &args[0];
264        let json_value: serde_json::Value = value.clone().try_into().ok()?;
265        let json_str = serde_json::to_string_pretty(&json_value).ok()?;
266
267        Some(Evaluation::String(json_str))
268    }
269
270    /// Constant-evaluates a `fromJSON(json_string)` call.
271    ///
272    /// See: <https://github.com/actions/languageservices/blob/1f3436c3cacc0f99d5d79e7120a5a9270cf13a72/expressions/src/funcs/fromjson.ts>
273    fn consteval_fromjson(args: &[Evaluation]) -> Option<Evaluation> {
274        if args.len() != 1 {
275            return None;
276        }
277
278        let json_str = args[0].sema().to_string();
279
280        // Match reference implementation: error on empty input
281        if json_str.trim().is_empty() {
282            return None;
283        }
284
285        serde_json::from_str::<serde_json::Value>(&json_str)
286            .ok()?
287            .try_into()
288            .ok()
289    }
290
291    /// Constant-evaluates a `join(array, optionalSeparator)` call.
292    ///
293    /// See: <https://github.com/actions/languageservices/blob/1f3436c3cacc0f99d5d79e7120a5a9270cf13a72/expressions/src/funcs/join.ts>
294    fn consteval_join(args: &[Evaluation]) -> Option<Evaluation> {
295        if args.is_empty() || args.len() > 2 {
296            return None;
297        }
298
299        let array_or_string = &args[0];
300
301        // Get separator (default is comma)
302        let separator = if args.len() > 1 {
303            args[1].sema().to_string()
304        } else {
305            ",".to_string()
306        };
307
308        match array_or_string {
309            // For primitive types (strings, numbers, booleans, null), return as string
310            Evaluation::String(_)
311            | Evaluation::Number(_)
312            | Evaluation::Boolean(_)
313            | Evaluation::Null => Some(Evaluation::String(array_or_string.sema().to_string())),
314            // For arrays, join elements with separator
315            Evaluation::Array(arr) => {
316                let joined = arr
317                    .iter()
318                    .map(|item| item.sema().to_string())
319                    .collect::<Vec<String>>()
320                    .join(&separator);
321                Some(Evaluation::String(joined))
322            }
323            // For dictionaries, return empty string (not supported in reference)
324            Evaluation::Object(_) => Some(Evaluation::String("".to_string())),
325        }
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use anyhow::Result;
332
333    use crate::{Expr, call::Call};
334
335    #[test]
336    fn test_consteval_fromjson() -> Result<()> {
337        use crate::Evaluation;
338
339        let test_cases = &[
340            // Basic primitives
341            ("fromJSON('null')", Evaluation::Null),
342            ("fromJSON('true')", Evaluation::Boolean(true)),
343            ("fromJSON('false')", Evaluation::Boolean(false)),
344            ("fromJSON('42')", Evaluation::Number(42.0)),
345            ("fromJSON('3.14')", Evaluation::Number(3.14)),
346            ("fromJSON('-0')", Evaluation::Number(0.0)),
347            ("fromJSON('0')", Evaluation::Number(0.0)),
348            (
349                "fromJSON('\"hello\"')",
350                Evaluation::String("hello".to_string()),
351            ),
352            ("fromJSON('\"\"')", Evaluation::String("".to_string())),
353            // Arrays
354            ("fromJSON('[]')", Evaluation::Array(vec![])),
355            (
356                "fromJSON('[1, 2, 3]')",
357                Evaluation::Array(vec![
358                    Evaluation::Number(1.0),
359                    Evaluation::Number(2.0),
360                    Evaluation::Number(3.0),
361                ]),
362            ),
363            (
364                "fromJSON('[\"a\", \"b\", null, true, 123]')",
365                Evaluation::Array(vec![
366                    Evaluation::String("a".to_string()),
367                    Evaluation::String("b".to_string()),
368                    Evaluation::Null,
369                    Evaluation::Boolean(true),
370                    Evaluation::Number(123.0),
371                ]),
372            ),
373            // Objects
374            (
375                "fromJSON('{}')",
376                Evaluation::Object(std::collections::HashMap::new()),
377            ),
378            (
379                "fromJSON('{\"key\": \"value\"}')",
380                Evaluation::Object({
381                    let mut map = std::collections::HashMap::new();
382                    map.insert("key".to_string(), Evaluation::String("value".to_string()));
383                    map
384                }),
385            ),
386            (
387                "fromJSON('{\"num\": 42, \"bool\": true, \"null\": null}')",
388                Evaluation::Object({
389                    let mut map = std::collections::HashMap::new();
390                    map.insert("num".to_string(), Evaluation::Number(42.0));
391                    map.insert("bool".to_string(), Evaluation::Boolean(true));
392                    map.insert("null".to_string(), Evaluation::Null);
393                    map
394                }),
395            ),
396            // Nested structures
397            (
398                "fromJSON('{\"array\": [1, 2], \"object\": {\"nested\": true}}')",
399                Evaluation::Object({
400                    let mut map = std::collections::HashMap::new();
401                    map.insert(
402                        "array".to_string(),
403                        Evaluation::Array(vec![Evaluation::Number(1.0), Evaluation::Number(2.0)]),
404                    );
405                    let mut nested_map = std::collections::HashMap::new();
406                    nested_map.insert("nested".to_string(), Evaluation::Boolean(true));
407                    map.insert("object".to_string(), Evaluation::Object(nested_map));
408                    map
409                }),
410            ),
411        ];
412
413        for (expr_str, expected) in test_cases {
414            let expr = Expr::parse(expr_str)?;
415            let result = expr.consteval().unwrap();
416            assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
417        }
418
419        Ok(())
420    }
421
422    #[test]
423    fn test_consteval_fromjson_error_cases() -> Result<()> {
424        let error_cases = &[
425            "fromJSON('')",          // Empty string
426            "fromJSON('   ')",       // Whitespace only
427            "fromJSON('invalid')",   // Invalid JSON
428            "fromJSON('{invalid}')", // Invalid JSON syntax
429            "fromJSON('[1, 2,]')",   // Trailing comma (invalid in strict JSON)
430        ];
431
432        for expr_str in error_cases {
433            let expr = Expr::parse(expr_str)?;
434            let result = expr.consteval();
435            assert!(
436                result.is_none(),
437                "Expected None for invalid JSON: {}",
438                expr_str
439            );
440        }
441
442        Ok(())
443    }
444
445    #[test]
446    fn test_consteval_fromjson_display_format() -> Result<()> {
447        use crate::Evaluation;
448
449        let test_cases = &[
450            (Evaluation::Array(vec![Evaluation::Number(1.0)]), "Array"),
451            (
452                Evaluation::Object(std::collections::HashMap::new()),
453                "Object",
454            ),
455        ];
456
457        for (result, expected) in test_cases {
458            assert_eq!(result.sema().to_string(), *expected);
459        }
460
461        Ok(())
462    }
463
464    #[test]
465    fn test_consteval_tojson_fromjson_roundtrip() -> Result<()> {
466        use crate::Evaluation;
467
468        // Test round-trip conversion for complex structures
469        let test_cases = &[
470            // Simple array
471            "[1, 2, 3]",
472            // Simple object
473            r#"{"key": "value"}"#,
474            // Mixed array
475            r#"[1, "hello", true, null]"#,
476            // Nested structure
477            r#"{"array": [1, 2], "object": {"nested": true}}"#,
478        ];
479
480        for json_str in test_cases {
481            // Parse with fromJSON
482            let from_expr_str = format!("fromJSON('{}')", json_str);
483            let from_expr = Expr::parse(&from_expr_str)?;
484            let parsed = from_expr.consteval().unwrap();
485
486            // Convert back with toJSON (using a dummy toJSON call structure)
487            let to_result = Call::consteval_tojson(&[parsed.clone()]).unwrap();
488
489            // Parse the result again to compare structure
490            let reparsed_expr_str = format!("fromJSON('{}')", to_result.sema().to_string());
491            let reparsed_expr = Expr::parse(&reparsed_expr_str)?;
492            let reparsed = reparsed_expr.consteval().unwrap();
493
494            // The structure should be preserved (though ordering might differ for objects)
495            match (&parsed, &reparsed) {
496                (Evaluation::Array(a), Evaluation::Array(b)) => assert_eq!(a, b),
497                (Evaluation::Object(_), Evaluation::Object(_)) => {
498                    // For dictionaries, we just check that both are dictionaries
499                    // since ordering might differ
500                    assert!(matches!(parsed, Evaluation::Object(_)));
501                    assert!(matches!(reparsed, Evaluation::Object(_)));
502                }
503                (a, b) => assert_eq!(a, b),
504            }
505        }
506
507        Ok(())
508    }
509
510    #[test]
511    fn test_consteval_format() -> Result<()> {
512        use crate::Evaluation;
513
514        let test_cases = &[
515            // Basic formatting
516            (
517                "format('Hello {0}', 'world')",
518                Evaluation::String("Hello world".to_string()),
519            ),
520            (
521                "format('{0} {1}', 'Hello', 'world')",
522                Evaluation::String("Hello world".to_string()),
523            ),
524            (
525                "format('Value: {0}', 42)",
526                Evaluation::String("Value: 42".to_string()),
527            ),
528            // Escaped braces
529            (
530                "format('{{0}}', 'test')",
531                Evaluation::String("{0}".to_string()),
532            ),
533            (
534                "format('{{Hello}} {0}', 'world')",
535                Evaluation::String("{Hello} world".to_string()),
536            ),
537            (
538                "format('{0} {{1}}', 'Hello')",
539                Evaluation::String("Hello {1}".to_string()),
540            ),
541            (
542                "format('}}{{', 'test')",
543                Evaluation::String("}{".to_string()),
544            ),
545            (
546                "format('{{{{}}}}', 'test')",
547                Evaluation::String("{{}}".to_string()),
548            ),
549            // Multiple arguments
550            (
551                "format('{0} {1} {2}', 'a', 'b', 'c')",
552                Evaluation::String("a b c".to_string()),
553            ),
554            (
555                "format('{2} {1} {0}', 'a', 'b', 'c')",
556                Evaluation::String("c b a".to_string()),
557            ),
558            // Repeated arguments
559            (
560                "format('{0} {0} {0}', 'test')",
561                Evaluation::String("test test test".to_string()),
562            ),
563            // No arguments to replace
564            (
565                "format('Hello world')",
566                Evaluation::String("Hello world".to_string()),
567            ),
568            // Trailing fragments
569            ("format('abc {{')", Evaluation::String("abc {".to_string())),
570            ("format('abc }}')", Evaluation::String("abc }".to_string())),
571            (
572                "format('abc {{}}')",
573                Evaluation::String("abc {}".to_string()),
574            ),
575        ];
576
577        for (expr_str, expected) in test_cases {
578            let expr = Expr::parse(expr_str)?;
579            let result = expr.consteval().unwrap();
580            assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
581        }
582
583        Ok(())
584    }
585
586    #[test]
587    fn test_consteval_format_error_cases() -> Result<()> {
588        let error_cases = &[
589            // Invalid format strings
590            "format('{0', 'test')",        // Missing closing brace
591            "format('0}', 'test')",        // Missing opening brace
592            "format('{a}', 'test')",       // Non-numeric placeholder
593            "format('{1}', 'test')",       // Argument index out of bounds
594            "format('{0} {2}', 'a', 'b')", // Argument index out of bounds
595            "format('{}', 'test')",        // Empty braces
596            "format('{-1}', 'test')",      // Negative index (invalid)
597        ];
598
599        for expr_str in error_cases {
600            let expr = Expr::parse(expr_str)?;
601            let result = expr.consteval();
602            assert!(
603                result.is_none(),
604                "Expected None for invalid format string: {}",
605                expr_str
606            );
607        }
608
609        Ok(())
610    }
611
612    #[test]
613    fn test_consteval_contains() -> Result<()> {
614        use crate::Evaluation;
615
616        let test_cases = &[
617            // Basic string contains (case-insensitive)
618            (
619                "contains('hello world', 'world')",
620                Evaluation::Boolean(true),
621            ),
622            (
623                "contains('hello world', 'WORLD')",
624                Evaluation::Boolean(true),
625            ),
626            (
627                "contains('HELLO WORLD', 'world')",
628                Evaluation::Boolean(true),
629            ),
630            ("contains('hello world', 'foo')", Evaluation::Boolean(false)),
631            ("contains('test', '')", Evaluation::Boolean(true)),
632            // Number to string conversion
633            ("contains('123', '2')", Evaluation::Boolean(true)),
634            ("contains(123, '2')", Evaluation::Boolean(true)),
635            ("contains('hello123', 123)", Evaluation::Boolean(true)),
636            // Boolean to string conversion
637            ("contains('true', true)", Evaluation::Boolean(true)),
638            ("contains('false', false)", Evaluation::Boolean(true)),
639            // Null handling
640            ("contains('null', null)", Evaluation::Boolean(true)),
641            ("contains(null, '')", Evaluation::Boolean(true)),
642            // Array contains - exact matches
643            (
644                "contains(fromJSON('[1, 2, 3]'), 2)",
645                Evaluation::Boolean(true),
646            ),
647            (
648                "contains(fromJSON('[1, 2, 3]'), 4)",
649                Evaluation::Boolean(false),
650            ),
651            (
652                "contains(fromJSON('[\"a\", \"b\", \"c\"]'), 'b')",
653                Evaluation::Boolean(true),
654            ),
655            (
656                "contains(fromJSON('[\"a\", \"b\", \"c\"]'), 'B')",
657                Evaluation::Boolean(false), // Array search is exact match, not case-insensitive
658            ),
659            (
660                "contains(fromJSON('[true, false, null]'), true)",
661                Evaluation::Boolean(true),
662            ),
663            (
664                "contains(fromJSON('[true, false, null]'), null)",
665                Evaluation::Boolean(true),
666            ),
667            // Empty array
668            (
669                "contains(fromJSON('[]'), 'anything')",
670                Evaluation::Boolean(false),
671            ),
672            // Mixed type array
673            (
674                "contains(fromJSON('[1, \"hello\", true, null]'), 'hello')",
675                Evaluation::Boolean(true),
676            ),
677            (
678                "contains(fromJSON('[1, \"hello\", true, null]'), 1)",
679                Evaluation::Boolean(true),
680            ),
681        ];
682
683        for (expr_str, expected) in test_cases {
684            let expr = Expr::parse(expr_str)?;
685            let result = expr.consteval().unwrap();
686            assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
687        }
688
689        Ok(())
690    }
691
692    #[test]
693    fn test_consteval_join() -> Result<()> {
694        use crate::Evaluation;
695
696        let test_cases = &[
697            // Basic array joining with default separator
698            (
699                "join(fromJSON('[\"a\", \"b\", \"c\"]'))",
700                Evaluation::String("a,b,c".to_string()),
701            ),
702            (
703                "join(fromJSON('[1, 2, 3]'))",
704                Evaluation::String("1,2,3".to_string()),
705            ),
706            (
707                "join(fromJSON('[true, false, null]'))",
708                Evaluation::String("true,false,".to_string()),
709            ),
710            // Array joining with custom separator
711            (
712                "join(fromJSON('[\"a\", \"b\", \"c\"]'), ' ')",
713                Evaluation::String("a b c".to_string()),
714            ),
715            (
716                "join(fromJSON('[1, 2, 3]'), '-')",
717                Evaluation::String("1-2-3".to_string()),
718            ),
719            (
720                "join(fromJSON('[\"hello\", \"world\"]'), ' | ')",
721                Evaluation::String("hello | world".to_string()),
722            ),
723            (
724                "join(fromJSON('[\"a\", \"b\", \"c\"]'), '')",
725                Evaluation::String("abc".to_string()),
726            ),
727            // Empty array
728            ("join(fromJSON('[]'))", Evaluation::String("".to_string())),
729            (
730                "join(fromJSON('[]'), '-')",
731                Evaluation::String("".to_string()),
732            ),
733            // Single element array
734            (
735                "join(fromJSON('[\"single\"]'))",
736                Evaluation::String("single".to_string()),
737            ),
738            (
739                "join(fromJSON('[\"single\"]'), '-')",
740                Evaluation::String("single".to_string()),
741            ),
742            // Primitive values (should return the value as string)
743            ("join('hello')", Evaluation::String("hello".to_string())),
744            (
745                "join('hello', '-')",
746                Evaluation::String("hello".to_string()),
747            ),
748            ("join(123)", Evaluation::String("123".to_string())),
749            ("join(true)", Evaluation::String("true".to_string())),
750            ("join(null)", Evaluation::String("".to_string())),
751            // Mixed type array
752            (
753                "join(fromJSON('[1, \"hello\", true, null]'))",
754                Evaluation::String("1,hello,true,".to_string()),
755            ),
756            (
757                "join(fromJSON('[1, \"hello\", true, null]'), ' | ')",
758                Evaluation::String("1 | hello | true | ".to_string()),
759            ),
760            // Special separator values
761            (
762                "join(fromJSON('[\"a\", \"b\", \"c\"]'), 123)",
763                Evaluation::String("a123b123c".to_string()),
764            ),
765            (
766                "join(fromJSON('[\"a\", \"b\", \"c\"]'), true)",
767                Evaluation::String("atruebtruec".to_string()),
768            ),
769        ];
770
771        for (expr_str, expected) in test_cases {
772            let expr = Expr::parse(expr_str)?;
773            let result = expr.consteval().unwrap();
774            assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
775        }
776
777        Ok(())
778    }
779
780    #[test]
781    fn test_consteval_endswith() -> Result<()> {
782        use crate::Evaluation;
783
784        let test_cases = &[
785            // Basic case-insensitive string endsWith
786            (
787                "endsWith('hello world', 'world')",
788                Evaluation::Boolean(true),
789            ),
790            (
791                "endsWith('hello world', 'WORLD')",
792                Evaluation::Boolean(true),
793            ),
794            (
795                "endsWith('HELLO WORLD', 'world')",
796                Evaluation::Boolean(true),
797            ),
798            (
799                "endsWith('hello world', 'hello')",
800                Evaluation::Boolean(false),
801            ),
802            ("endsWith('hello world', 'foo')", Evaluation::Boolean(false)),
803            // Empty string cases
804            ("endsWith('test', '')", Evaluation::Boolean(true)),
805            ("endsWith('', '')", Evaluation::Boolean(true)),
806            ("endsWith('', 'test')", Evaluation::Boolean(false)),
807            // Number to string conversion
808            ("endsWith('123', '3')", Evaluation::Boolean(true)),
809            ("endsWith(123, '3')", Evaluation::Boolean(true)),
810            ("endsWith('hello123', 123)", Evaluation::Boolean(true)),
811            ("endsWith(12345, 345)", Evaluation::Boolean(true)),
812            // Boolean to string conversion
813            ("endsWith('test true', true)", Evaluation::Boolean(true)),
814            ("endsWith('test false', false)", Evaluation::Boolean(true)),
815            ("endsWith(true, 'ue')", Evaluation::Boolean(true)),
816            // Null handling
817            ("endsWith('test null', null)", Evaluation::Boolean(true)),
818            ("endsWith(null, '')", Evaluation::Boolean(true)),
819            ("endsWith('something', null)", Evaluation::Boolean(true)), // null converts to empty string
820            // Non-primitive types should return false
821            (
822                "endsWith(fromJSON('[1, 2, 3]'), '3')",
823                Evaluation::Boolean(false),
824            ),
825            (
826                "endsWith('test', fromJSON('[1, 2, 3]'))",
827                Evaluation::Boolean(false),
828            ),
829            (
830                "endsWith(fromJSON('{\"key\": \"value\"}'), 'value')",
831                Evaluation::Boolean(false),
832            ),
833            (
834                "endsWith('test', fromJSON('{\"key\": \"value\"}'))",
835                Evaluation::Boolean(false),
836            ),
837            // Mixed case scenarios
838            (
839                "endsWith('TestString', 'STRING')",
840                Evaluation::Boolean(true),
841            ),
842            ("endsWith('CamelCase', 'case')", Evaluation::Boolean(true)),
843            // Exact match
844            ("endsWith('exact', 'exact')", Evaluation::Boolean(true)),
845            // Longer suffix than string
846            (
847                "endsWith('short', 'very long suffix')",
848                Evaluation::Boolean(false),
849            ),
850        ];
851
852        for (expr_str, expected) in test_cases {
853            let expr = Expr::parse(expr_str)?;
854            let result = expr.consteval().unwrap();
855            assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
856        }
857
858        Ok(())
859    }
860
861    #[test]
862    fn test_consteval_startswith() -> Result<()> {
863        use crate::Evaluation;
864
865        let test_cases = &[
866            // Basic case-insensitive string startsWith
867            (
868                "startsWith('hello world', 'hello')",
869                Evaluation::Boolean(true),
870            ),
871            (
872                "startsWith('hello world', 'HELLO')",
873                Evaluation::Boolean(true),
874            ),
875            (
876                "startsWith('HELLO WORLD', 'hello')",
877                Evaluation::Boolean(true),
878            ),
879            (
880                "startsWith('hello world', 'world')",
881                Evaluation::Boolean(false),
882            ),
883            (
884                "startsWith('hello world', 'foo')",
885                Evaluation::Boolean(false),
886            ),
887            // Empty string cases
888            ("startsWith('test', '')", Evaluation::Boolean(true)),
889            ("startsWith('', '')", Evaluation::Boolean(true)),
890            ("startsWith('', 'test')", Evaluation::Boolean(false)),
891            // Number to string conversion
892            ("startsWith('123', '1')", Evaluation::Boolean(true)),
893            ("startsWith(123, '1')", Evaluation::Boolean(true)),
894            ("startsWith('123hello', 123)", Evaluation::Boolean(true)),
895            ("startsWith(12345, 123)", Evaluation::Boolean(true)),
896            // Boolean to string conversion
897            ("startsWith('true test', true)", Evaluation::Boolean(true)),
898            ("startsWith('false test', false)", Evaluation::Boolean(true)),
899            ("startsWith(true, 'tr')", Evaluation::Boolean(true)),
900            // Null handling
901            ("startsWith('null test', null)", Evaluation::Boolean(true)),
902            ("startsWith(null, '')", Evaluation::Boolean(true)),
903            (
904                "startsWith('something', null)",
905                Evaluation::Boolean(true), // null converts to empty string
906            ),
907            // Non-primitive types should return false
908            (
909                "startsWith(fromJSON('[1, 2, 3]'), '1')",
910                Evaluation::Boolean(false),
911            ),
912            (
913                "startsWith('test', fromJSON('[1, 2, 3]'))",
914                Evaluation::Boolean(false),
915            ),
916            (
917                "startsWith(fromJSON('{\"key\": \"value\"}'), 'key')",
918                Evaluation::Boolean(false),
919            ),
920            (
921                "startsWith('test', fromJSON('{\"key\": \"value\"}'))",
922                Evaluation::Boolean(false),
923            ),
924            // Mixed case scenarios
925            (
926                "startsWith('TestString', 'TEST')",
927                Evaluation::Boolean(true),
928            ),
929            (
930                "startsWith('CamelCase', 'camel')",
931                Evaluation::Boolean(true),
932            ),
933            // Exact match
934            ("startsWith('exact', 'exact')", Evaluation::Boolean(true)),
935            // Longer prefix than string
936            (
937                "startsWith('short', 'very long prefix')",
938                Evaluation::Boolean(false),
939            ),
940            // Partial matches
941            (
942                "startsWith('prefix_suffix', 'prefix')",
943                Evaluation::Boolean(true),
944            ),
945            (
946                "startsWith('prefix_suffix', 'suffix')",
947                Evaluation::Boolean(false),
948            ),
949        ];
950
951        for (expr_str, expected) in test_cases {
952            let expr = Expr::parse(expr_str)?;
953            let result = expr.consteval().unwrap();
954            assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
955        }
956
957        Ok(())
958    }
959
960    #[test]
961    fn test_evaluate_constant_functions() -> Result<()> {
962        use crate::Evaluation;
963
964        let test_cases = &[
965            // format function
966            (
967                "format('{0}', 'hello')",
968                Evaluation::String("hello".to_string()),
969            ),
970            (
971                "format('{0} {1}', 'hello', 'world')",
972                Evaluation::String("hello world".to_string()),
973            ),
974            (
975                "format('Value: {0}', 42)",
976                Evaluation::String("Value: 42".to_string()),
977            ),
978            // contains function
979            (
980                "contains('hello world', 'world')",
981                Evaluation::Boolean(true),
982            ),
983            ("contains('hello world', 'foo')", Evaluation::Boolean(false)),
984            ("contains('test', '')", Evaluation::Boolean(true)),
985            // startsWith function
986            (
987                "startsWith('hello world', 'hello')",
988                Evaluation::Boolean(true),
989            ),
990            (
991                "startsWith('hello world', 'world')",
992                Evaluation::Boolean(false),
993            ),
994            ("startsWith('test', '')", Evaluation::Boolean(true)),
995            // endsWith function
996            (
997                "endsWith('hello world', 'world')",
998                Evaluation::Boolean(true),
999            ),
1000            (
1001                "endsWith('hello world', 'hello')",
1002                Evaluation::Boolean(false),
1003            ),
1004            ("endsWith('test', '')", Evaluation::Boolean(true)),
1005            // toJSON function
1006            (
1007                "toJSON('hello')",
1008                Evaluation::String("\"hello\"".to_string()),
1009            ),
1010            ("toJSON(42)", Evaluation::String("42".to_string())),
1011            ("toJSON(true)", Evaluation::String("true".to_string())),
1012            ("toJSON(null)", Evaluation::String("null".to_string())),
1013            // fromJSON function - primitives
1014            (
1015                "fromJSON('\"hello\"')",
1016                Evaluation::String("hello".to_string()),
1017            ),
1018            ("fromJSON('42')", Evaluation::Number(42.0)),
1019            ("fromJSON('true')", Evaluation::Boolean(true)),
1020            ("fromJSON('null')", Evaluation::Null),
1021            // fromJSON function - arrays and objects
1022            (
1023                "fromJSON('[1, 2, 3]')",
1024                Evaluation::Array(vec![
1025                    Evaluation::Number(1.0),
1026                    Evaluation::Number(2.0),
1027                    Evaluation::Number(3.0),
1028                ]),
1029            ),
1030            (
1031                "fromJSON('{\"key\": \"value\"}')",
1032                Evaluation::Object({
1033                    let mut map = std::collections::HashMap::new();
1034                    map.insert("key".to_string(), Evaluation::String("value".to_string()));
1035                    map
1036                }),
1037            ),
1038        ];
1039
1040        for (expr_str, expected) in test_cases {
1041            let expr = Expr::parse(expr_str)?;
1042            let result = expr.consteval().unwrap();
1043            assert_eq!(
1044                result, *expected,
1045                "Failed for expression: {} {result:?}",
1046                expr_str
1047            );
1048        }
1049
1050        Ok(())
1051    }
1052}