vrl/stdlib/
starts_with.rs

1use crate::compiler::prelude::*;
2use std::sync::LazyLock;
3
4static DEFAULT_CASE_SENSITIVE: LazyLock<Value> = LazyLock::new(|| Value::Boolean(true));
5
6static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
7    vec![
8        Parameter::required("value", kind::BYTES, "The string to search."),
9        Parameter::required(
10            "substring",
11            kind::BYTES,
12            "The substring that the `value` must start with.",
13        ),
14        Parameter::optional(
15            "case_sensitive",
16            kind::BOOLEAN,
17            "Whether the match should be case sensitive.",
18        )
19        .default(&DEFAULT_CASE_SENSITIVE),
20    ]
21});
22
23struct Chars<'a> {
24    bytes: &'a Bytes,
25    pos: usize,
26}
27
28impl<'a> Chars<'a> {
29    fn new(bytes: &'a Bytes) -> Self {
30        Self { bytes, pos: 0 }
31    }
32}
33
34impl Iterator for Chars<'_> {
35    type Item = std::result::Result<char, u8>;
36
37    fn next(&mut self) -> Option<Self::Item> {
38        if self.pos >= self.bytes.len() {
39            return None;
40        }
41
42        let width = utf8_width::get_width(self.bytes[self.pos]);
43        if width == 1 {
44            self.pos += 1;
45            Some(Ok(self.bytes[self.pos - 1] as char))
46        } else {
47            let c = std::str::from_utf8(&self.bytes[self.pos..self.pos + width]);
48            if let Ok(chr) = c {
49                self.pos += width;
50                Some(Ok(chr.chars().next().unwrap()))
51            } else {
52                self.pos += 1;
53                Some(Err(self.bytes[self.pos]))
54            }
55        }
56    }
57}
58
59#[derive(Clone, Copy)]
60enum Case {
61    Sensitive,
62    Insensitive,
63}
64
65fn starts_with(bytes: &Bytes, starts: &Bytes, case: Case) -> bool {
66    if bytes.len() < starts.len() {
67        return false;
68    }
69
70    match case {
71        Case::Sensitive => starts[..] == bytes[0..starts.len()],
72        Case::Insensitive => Chars::new(starts)
73            .zip(Chars::new(bytes))
74            .all(|(a, b)| match (a, b) {
75                (Ok(a), Ok(b)) => {
76                    if a.is_ascii() && b.is_ascii() {
77                        a.eq_ignore_ascii_case(&b)
78                    } else {
79                        a.to_lowercase().zip(b.to_lowercase()).all(|(a, b)| a == b)
80                    }
81                }
82                _ => false,
83            }),
84    }
85}
86
87#[derive(Clone, Copy, Debug)]
88pub struct StartsWith;
89
90impl Function for StartsWith {
91    fn identifier(&self) -> &'static str {
92        "starts_with"
93    }
94
95    fn usage(&self) -> &'static str {
96        "Determines whether `value` begins with `substring`."
97    }
98
99    fn category(&self) -> &'static str {
100        Category::String.as_ref()
101    }
102
103    fn return_kind(&self) -> u16 {
104        kind::BOOLEAN
105    }
106
107    fn parameters(&self) -> &'static [Parameter] {
108        PARAMETERS.as_slice()
109    }
110
111    fn examples(&self) -> &'static [Example] {
112        &[
113            example! {
114                title: "String starts with (case sensitive)",
115                source: r#"starts_with("The Needle In The Haystack", "The Needle")"#,
116                result: Ok("true"),
117            },
118            example! {
119                title: "String starts with (case insensitive)",
120                source: r#"starts_with("The Needle In The Haystack", "the needle", case_sensitive: false)"#,
121                result: Ok("true"),
122            },
123            example! {
124                title: "String starts with (case sensitive failure)",
125                source: r#"starts_with("foobar", "F")"#,
126                result: Ok("false"),
127            },
128        ]
129    }
130
131    fn compile(
132        &self,
133        _state: &state::TypeState,
134        _ctx: &mut FunctionCompileContext,
135        arguments: ArgumentList,
136    ) -> Compiled {
137        let value = arguments.required("value");
138        let substring = arguments.required("substring");
139        let case_sensitive = arguments.optional("case_sensitive");
140
141        Ok(StartsWithFn {
142            value,
143            substring,
144            case_sensitive,
145        }
146        .as_expr())
147    }
148}
149
150#[derive(Debug, Clone)]
151struct StartsWithFn {
152    value: Box<dyn Expression>,
153    substring: Box<dyn Expression>,
154    case_sensitive: Option<Box<dyn Expression>>,
155}
156
157impl FunctionExpression for StartsWithFn {
158    fn resolve(&self, ctx: &mut Context) -> Resolved {
159        let case_sensitive = self
160            .case_sensitive
161            .map_resolve_with_default(ctx, || DEFAULT_CASE_SENSITIVE.clone())?
162            .try_boolean()?;
163        let case_sensitive = if case_sensitive {
164            Case::Sensitive
165        } else {
166            Case::Insensitive
167        };
168
169        let substring = self.substring.resolve(ctx)?;
170        let substring = substring.try_bytes()?;
171
172        let value = self.value.resolve(ctx)?;
173        let value = value.try_bytes()?;
174
175        Ok(starts_with(&value, &substring, case_sensitive).into())
176    }
177
178    fn type_def(&self, _: &state::TypeState) -> TypeDef {
179        TypeDef::boolean().infallible()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    test_function![
188        starts_with => StartsWith;
189
190        no {
191            args: func_args![value: "foo",
192                             substring: "bar"
193            ],
194            want: Ok(false),
195            tdef: TypeDef::boolean().infallible(),
196        }
197
198        subset {
199            args: func_args![value: "foo",
200                             substring: "foobar"
201            ],
202            want: Ok(false),
203            tdef: TypeDef::boolean().infallible(),
204        }
205
206        total {
207            args: func_args![value: "foo",
208                             substring: "foo"
209            ],
210            want: Ok(true),
211            tdef: TypeDef::boolean().infallible(),
212        }
213
214        middle {
215            args: func_args![value: "foobar",
216                             substring: "oba"
217            ],
218            want: Ok(false),
219            tdef: TypeDef::boolean().infallible(),
220        }
221
222        start {
223            args: func_args![value: "foobar",
224                             substring: "foo"
225            ],
226            want: Ok(true),
227            tdef: TypeDef::boolean().infallible(),
228        }
229
230        end {
231            args: func_args![value: "foobar",
232                             substring: "bar"
233            ],
234            want: Ok(false),
235            tdef: TypeDef::boolean().infallible(),
236        }
237
238
239        case_sensitive_same_case {
240            args: func_args![value: "FOObar",
241                             substring: "FOO"
242            ],
243            want: Ok(true),
244            tdef: TypeDef::boolean().infallible(),
245        }
246
247        case_sensitive_different_case {
248            args: func_args![value: "foobar",
249                             substring: "FOO"
250            ],
251            want: Ok(false),
252            tdef: TypeDef::boolean().infallible(),
253        }
254
255        case_insensitive_different_case {
256            args: func_args![value: "foobar",
257                             substring: "FOO",
258                             case_sensitive: false
259            ],
260            want: Ok(true),
261            tdef: TypeDef::boolean().infallible(),
262        }
263
264        unicode_same_case {
265            args: func_args![value: "𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙Ꮺ믚㋫𐠘𒃪𖾛𞺘ᰙꢝⶺ觨⨙ઉzook",
266                             substring: "𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙",
267                             case_sensitive: true
268            ],
269            want: Ok(true),
270            tdef: TypeDef::boolean().infallible(),
271        }
272
273        unicode_sensitive_different_case {
274            args: func_args![value: "ξ𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙Ꮺ믚㋫𐠘𒃪𖾛𞺘ᰙꢝⶺ觨⨙ઉzook",
275                             substring: "Ξ𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙",
276                             case_sensitive: true
277            ],
278            want: Ok(false),
279            tdef: TypeDef::boolean().infallible(),
280        }
281
282        unicode_insensitive_different_case {
283            args: func_args![value: "ξ𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙Ꮺ믚㋫𐠘𒃪𖾛𞺘ᰙꢝⶺ觨⨙ઉzook",
284                             substring: "Ξ𛋙ၺ㚺𛋙Zonkکᤊᰙ𛋙",
285                             case_sensitive: false
286            ],
287            want: Ok(true),
288            tdef: TypeDef::boolean().infallible(),
289        }
290    ];
291}