Skip to main content

cel_core/ext/
string_ext.rs

1//! String extension library for CEL.
2//!
3//! This module provides additional string manipulation functions beyond the
4//! CEL standard library, matching the cel-go strings extension.
5//!
6//! # Functions
7//!
8//! - `charAt(index)` - Returns character at index as a string
9//! - `indexOf(substring)` / `indexOf(substring, offset)` - Find first occurrence
10//! - `lastIndexOf(substring)` / `lastIndexOf(substring, offset)` - Find last occurrence
11//! - `lowerAscii()` - Convert ASCII characters to lowercase
12//! - `upperAscii()` - Convert ASCII characters to uppercase
13//! - `replace(old, new)` / `replace(old, new, count)` - Replace occurrences
14//! - `split(separator)` / `split(separator, limit)` - Split string into list
15//! - `substring(start)` / `substring(start, end)` - Extract substring
16//! - `trim()` - Remove leading/trailing whitespace
17//! - `reverse()` - Reverse the string (Unicode-aware)
18//! - `format(args)` - Format string with arguments
19//! - `join()` / `join(separator)` - Join list of strings (method on list<string>)
20//! - `strings.quote(string)` - Quote a string with escapes
21
22use crate::types::{CelType, FunctionDecl, OverloadDecl};
23
24/// Returns the string extension library function declarations.
25pub fn string_extension() -> Vec<FunctionDecl> {
26    vec![
27        // charAt: (string).charAt(int) -> string
28        FunctionDecl::new("charAt").with_overload(OverloadDecl::method(
29            "string_char_at_int",
30            vec![CelType::String, CelType::Int],
31            CelType::String,
32        )),
33        // indexOf: two overloads
34        FunctionDecl::new("indexOf")
35            .with_overload(OverloadDecl::method(
36                "string_index_of_string",
37                vec![CelType::String, CelType::String],
38                CelType::Int,
39            ))
40            .with_overload(OverloadDecl::method(
41                "string_index_of_string_int",
42                vec![CelType::String, CelType::String, CelType::Int],
43                CelType::Int,
44            )),
45        // lastIndexOf
46        FunctionDecl::new("lastIndexOf")
47            .with_overload(OverloadDecl::method(
48                "string_last_index_of_string",
49                vec![CelType::String, CelType::String],
50                CelType::Int,
51            ))
52            .with_overload(OverloadDecl::method(
53                "string_last_index_of_string_int",
54                vec![CelType::String, CelType::String, CelType::Int],
55                CelType::Int,
56            )),
57        // lowerAscii
58        FunctionDecl::new("lowerAscii").with_overload(OverloadDecl::method(
59            "string_lower_ascii",
60            vec![CelType::String],
61            CelType::String,
62        )),
63        // upperAscii
64        FunctionDecl::new("upperAscii").with_overload(OverloadDecl::method(
65            "string_upper_ascii",
66            vec![CelType::String],
67            CelType::String,
68        )),
69        // replace
70        FunctionDecl::new("replace")
71            .with_overload(OverloadDecl::method(
72                "string_replace_string_string",
73                vec![CelType::String, CelType::String, CelType::String],
74                CelType::String,
75            ))
76            .with_overload(OverloadDecl::method(
77                "string_replace_string_string_int",
78                vec![CelType::String, CelType::String, CelType::String, CelType::Int],
79                CelType::String,
80            )),
81        // split
82        FunctionDecl::new("split")
83            .with_overload(OverloadDecl::method(
84                "string_split_string",
85                vec![CelType::String, CelType::String],
86                CelType::list(CelType::String),
87            ))
88            .with_overload(OverloadDecl::method(
89                "string_split_string_int",
90                vec![CelType::String, CelType::String, CelType::Int],
91                CelType::list(CelType::String),
92            )),
93        // substring
94        FunctionDecl::new("substring")
95            .with_overload(OverloadDecl::method(
96                "string_substring_int",
97                vec![CelType::String, CelType::Int],
98                CelType::String,
99            ))
100            .with_overload(OverloadDecl::method(
101                "string_substring_int_int",
102                vec![CelType::String, CelType::Int, CelType::Int],
103                CelType::String,
104            )),
105        // trim
106        FunctionDecl::new("trim").with_overload(OverloadDecl::method(
107            "string_trim",
108            vec![CelType::String],
109            CelType::String,
110        )),
111        // reverse
112        FunctionDecl::new("reverse").with_overload(OverloadDecl::method(
113            "string_reverse",
114            vec![CelType::String],
115            CelType::String,
116        )),
117        // format
118        FunctionDecl::new("format").with_overload(OverloadDecl::method(
119            "string_format",
120            vec![CelType::String, CelType::list(CelType::Dyn)],
121            CelType::String,
122        )),
123        // join - method on list<string>
124        FunctionDecl::new("join")
125            .with_overload(OverloadDecl::method(
126                "list_string_join",
127                vec![CelType::list(CelType::String)],
128                CelType::String,
129            ))
130            .with_overload(OverloadDecl::method(
131                "list_string_join_string",
132                vec![CelType::list(CelType::String), CelType::String],
133                CelType::String,
134            )),
135        // strings.quote - namespaced standalone function
136        FunctionDecl::new("strings.quote").with_overload(OverloadDecl::function(
137            "strings_quote_string",
138            vec![CelType::String],
139            CelType::String,
140        )),
141    ]
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_string_extension_count() {
150        let funcs = string_extension();
151        // 13 functions defined
152        assert_eq!(funcs.len(), 13);
153    }
154
155    #[test]
156    fn test_char_at() {
157        let funcs = string_extension();
158        let char_at = funcs.iter().find(|f| f.name == "charAt").unwrap();
159        assert_eq!(char_at.overloads.len(), 1);
160        assert!(char_at.overloads[0].is_member);
161    }
162
163    #[test]
164    fn test_index_of_overloads() {
165        let funcs = string_extension();
166        let index_of = funcs.iter().find(|f| f.name == "indexOf").unwrap();
167        assert_eq!(index_of.overloads.len(), 2);
168    }
169
170    #[test]
171    fn test_join_is_member_on_list() {
172        let funcs = string_extension();
173        let join = funcs.iter().find(|f| f.name == "join").unwrap();
174        assert_eq!(join.overloads.len(), 2);
175        for overload in &join.overloads {
176            assert!(overload.is_member);
177            assert_eq!(overload.receiver_type(), Some(&CelType::list(CelType::String)));
178        }
179    }
180
181    #[test]
182    fn test_strings_quote_is_standalone() {
183        let funcs = string_extension();
184        let quote = funcs.iter().find(|f| f.name == "strings.quote").unwrap();
185        assert_eq!(quote.overloads.len(), 1);
186        assert!(!quote.overloads[0].is_member);
187    }
188}