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