vrl/stdlib/
slice.rs

1use std::ops::Range;
2
3use crate::compiler::prelude::*;
4use std::sync::LazyLock;
5
6static DEFAULT_END: LazyLock<Value> = LazyLock::new(|| Value::Bytes(Bytes::from("String length")));
7
8static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
9    vec![
10        Parameter::required(
11            "value",
12            kind::BYTES | kind::ARRAY,
13            "The string or array to slice.",
14        ),
15        Parameter::required(
16            "start",
17            kind::INTEGER,
18            "The inclusive start position. A zero-based index that can be negative.",
19        ),
20        Parameter::optional(
21            "end",
22            kind::INTEGER,
23            "The exclusive end position. A zero-based index that can be negative.",
24        )
25        .default(&DEFAULT_END),
26    ]
27});
28
29#[allow(clippy::cast_possible_wrap)]
30#[allow(clippy::cast_sign_loss)]
31#[allow(clippy::cast_possible_truncation)] //TODO evaluate removal options
32fn slice(start: i64, end: Option<i64>, value: Value) -> Resolved {
33    let range = |len: i64| -> ExpressionResult<Range<usize>> {
34        let start = match start {
35            start if start < 0 => start + len,
36            start => start,
37        };
38
39        let end = match end {
40            Some(end) if end < 0 => end + len,
41            Some(end) => end,
42            None => len,
43        };
44
45        match () {
46            () if start < 0 || start > len => {
47                Err(format!(r#""start" must be between "{}" and "{len}""#, -len).into())
48            }
49            () if end < start => Err(r#""end" must be greater or equal to "start""#.into()),
50            () if end > len => Ok(start as usize..len as usize),
51            () => Ok(start as usize..end as usize),
52        }
53    };
54    match value {
55        Value::Bytes(v) => range(v.len() as i64)
56            .map(|range| v.slice(range))
57            .map(Value::from),
58        Value::Array(mut v) => range(v.len() as i64)
59            .map(|range| v.drain(range).collect::<Vec<_>>())
60            .map(Value::from),
61        value => Err(ValueError::Expected {
62            got: value.kind(),
63            expected: Kind::bytes() | Kind::array(Collection::any()),
64        }
65        .into()),
66    }
67}
68
69#[derive(Clone, Copy, Debug)]
70pub struct Slice;
71
72impl Function for Slice {
73    fn identifier(&self) -> &'static str {
74        "slice"
75    }
76
77    fn usage(&self) -> &'static str {
78        indoc! {"
79            Returns a slice of `value` between the `start` and `end` positions.
80
81            If the `start` and `end` parameters are negative, they refer to positions counting from the right of the
82            string or array. If `end` refers to a position that is greater than the length of the string or array,
83            a slice up to the end of the string or array is returned.
84        "}
85    }
86
87    fn category(&self) -> &'static str {
88        Category::String.as_ref()
89    }
90
91    fn return_kind(&self) -> u16 {
92        kind::ARRAY | kind::BYTES
93    }
94
95    fn parameters(&self) -> &'static [Parameter] {
96        PARAMETERS.as_slice()
97    }
98
99    fn examples(&self) -> &'static [Example] {
100        &[
101            example! {
102                title: "Slice a string (positive index)",
103                source: r#"slice!("Supercalifragilisticexpialidocious", start: 5, end: 13)"#,
104                result: Ok("califrag"),
105            },
106            example! {
107                title: "Slice a string (negative index)",
108                source: r#"slice!("Supercalifragilisticexpialidocious", start: 5, end: -14)"#,
109                result: Ok("califragilistic"),
110            },
111            example! {
112                title: "String start",
113                source: r#"slice!("foobar", 3)"#,
114                result: Ok("bar"),
115            },
116            example! {
117                title: "Array start",
118                source: "slice!([0, 1, 2], 1)",
119                result: Ok("[1, 2]"),
120            },
121        ]
122    }
123
124    fn compile(
125        &self,
126        _state: &state::TypeState,
127        _ctx: &mut FunctionCompileContext,
128        arguments: ArgumentList,
129    ) -> Compiled {
130        let value = arguments.required("value");
131        let start = arguments.required("start");
132        let end = arguments.optional("end");
133
134        Ok(SliceFn { value, start, end }.as_expr())
135    }
136}
137
138#[derive(Debug, Clone)]
139struct SliceFn {
140    value: Box<dyn Expression>,
141    start: Box<dyn Expression>,
142    end: Option<Box<dyn Expression>>,
143}
144
145impl FunctionExpression for SliceFn {
146    fn resolve(&self, ctx: &mut Context) -> Resolved {
147        let start = self.start.resolve(ctx)?.try_integer()?;
148        let end = match &self.end {
149            Some(expr) => Some(expr.resolve(ctx)?.try_integer()?),
150            None => None,
151        };
152        let value = self.value.resolve(ctx)?;
153
154        slice(start, end, value)
155    }
156
157    fn type_def(&self, state: &state::TypeState) -> TypeDef {
158        let td = TypeDef::from(Kind::never()).fallible();
159
160        match self.value.type_def(state) {
161            v if v.is_bytes() => td.union(v),
162            v if v.is_array() => td.union(v),
163            _ => td.or_bytes().or_array(Collection::any()),
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use crate::value;
171    use std::collections::BTreeMap;
172
173    use super::*;
174
175    test_function![
176        slice => Slice;
177
178        bytes_0 {
179            args: func_args![value: "foo",
180                             start: 0
181            ],
182            want: Ok("foo"),
183            tdef: TypeDef::bytes().fallible(),
184        }
185
186        bytes_1 {
187            args: func_args![value: "foo",
188                             start: 1
189            ],
190            want: Ok("oo"),
191            tdef: TypeDef::bytes().fallible(),
192        }
193
194        bytes_2 {
195            args: func_args![value: "foo",
196                             start: 2
197            ],
198            want: Ok("o"),
199            tdef: TypeDef::bytes().fallible(),
200        }
201
202        bytes_minus_2 {
203            args: func_args![value: "foo",
204                             start: -2
205            ],
206            want: Ok("oo"),
207            tdef: TypeDef::bytes().fallible(),
208        }
209
210        bytes_empty {
211            args: func_args![value: "foo",
212                             start: 3
213            ],
214            want: Ok(""),
215            tdef: TypeDef::bytes().fallible(),
216        }
217
218        bytes_empty_start_end {
219            args: func_args![value: "foo",
220                             start: 2,
221                             end: 2
222            ],
223            want: Ok(""),
224            tdef: TypeDef::bytes().fallible(),
225        }
226
227        bytes_overrun {
228            args: func_args![value: "foo",
229                             start: 0,
230                             end: 4
231            ],
232            want: Ok("foo"),
233            tdef: TypeDef::bytes().fallible(),
234        }
235
236        bytes_start_overrun {
237            args: func_args![value: "foo",
238                             start: 1,
239                             end: 5
240            ],
241            want: Ok("oo"),
242            tdef: TypeDef::bytes().fallible(),
243        }
244
245        bytes_negative  {
246            args: func_args![value: "Supercalifragilisticexpialidocious",
247                             start: -7
248            ],
249            want: Ok("docious"),
250            tdef: TypeDef::bytes().fallible(),
251        }
252
253        bytes_middle {
254            args: func_args![value: "Supercalifragilisticexpialidocious",
255                             start: 5,
256                             end: 9
257            ],
258            want: Ok("cali"),
259            tdef: TypeDef::bytes().fallible(),
260        }
261
262        array_0 {
263            args: func_args![value: vec![0, 1, 2],
264                             start: 0
265            ],
266            want: Ok(vec![0, 1, 2]),
267            tdef: TypeDef::array(Collection::from_parts(BTreeMap::from([
268                (Index::from(0), Kind::integer()),
269                (Index::from(1), Kind::integer()),
270                (Index::from(2), Kind::integer()),
271            ]), Kind::undefined())).fallible(),
272        }
273
274        array_1 {
275            args: func_args![value: vec![0, 1, 2],
276                             start: 1
277            ],
278            want: Ok(vec![1, 2]),
279            tdef: TypeDef::array(Collection::from_parts(BTreeMap::from([
280                (Index::from(0), Kind::integer()),
281                (Index::from(1), Kind::integer()),
282                (Index::from(2), Kind::integer()),
283            ]), Kind::undefined())).fallible(),
284        }
285
286        array_minus_2 {
287            args: func_args![value: vec![0, 1, 2],
288                             start: -2
289            ],
290            want: Ok(vec![1, 2]),
291            tdef: TypeDef::array(Collection::from_parts(BTreeMap::from([
292                (Index::from(0), Kind::integer()),
293                (Index::from(1), Kind::integer()),
294                (Index::from(2), Kind::integer()),
295            ]), Kind::undefined())).fallible(),
296        }
297
298        array_mixed_types {
299            args: func_args![value: value!([0, "ook", true]),
300                             start: 1
301            ],
302            want: Ok(value!(["ook", true])),
303                       tdef: TypeDef::array(Collection::from_parts(BTreeMap::from([
304                (Index::from(0), Kind::integer()),
305                (Index::from(1), Kind::bytes()),
306                (Index::from(2), Kind::boolean()),
307            ]), Kind::undefined())).fallible(),
308        }
309
310        error_after_end {
311            args: func_args![value: "foo",
312                             start: 4
313            ],
314            want: Err(r#""start" must be between "-3" and "3""#),
315            tdef: TypeDef::bytes().fallible(),
316        }
317
318        error_minus_before_start {
319            args: func_args![value: "foo",
320                             start: -4
321            ],
322            want: Err(r#""start" must be between "-3" and "3""#),
323            tdef: TypeDef::bytes().fallible(),
324        }
325
326        error_start_end {
327            args: func_args![value: "foo",
328                             start: 2,
329                             end: 1
330            ],
331            want: Err(r#""end" must be greater or equal to "start""#),
332            tdef: TypeDef::bytes().fallible(),
333        }
334    ];
335}