vrl/stdlib/
truncate.rs

1use crate::compiler::prelude::*;
2
3fn truncate(value: &Value, limit: Value, suffix: &Value) -> Resolved {
4    let mut value = value.try_bytes_utf8_lossy()?.into_owned();
5    let limit = limit.try_integer()?;
6    // TODO consider removal options
7    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
8    let limit = if limit < 0 { 0 } else { limit as usize };
9    let suffix = suffix.try_bytes_utf8_lossy()?.to_string();
10    let pos = if let Some((pos, chr)) = value.char_indices().take(limit).last() {
11        // char_indices gives us the starting position of the character at limit,
12        // we want the end position.
13        pos + chr.len_utf8()
14    } else {
15        // We have an empty string
16        0
17    };
18    if value.len() > pos {
19        value.truncate(pos);
20        if !suffix.is_empty() {
21            value.push_str(&suffix);
22        }
23    }
24    Ok(value.into())
25}
26
27#[derive(Clone, Copy, Debug)]
28pub struct Truncate;
29
30impl Function for Truncate {
31    fn identifier(&self) -> &'static str {
32        "truncate"
33    }
34
35    fn usage(&self) -> &'static str {
36        "Truncates the `value` string up to the `limit` number of characters."
37    }
38
39    fn category(&self) -> &'static str {
40        Category::String.as_ref()
41    }
42
43    fn return_kind(&self) -> u16 {
44        kind::BYTES
45    }
46
47    fn return_rules(&self) -> &'static [&'static str] {
48        &["The string is returned unchanged its length is less than `limit`."]
49    }
50
51    fn parameters(&self) -> &'static [Parameter] {
52        const PARAMETERS: &[Parameter] = &[
53            Parameter::required("value", kind::BYTES, "The string to truncate."),
54            Parameter::required(
55                "limit",
56                kind::INTEGER,
57                "The number of characters to truncate the string after.",
58            ),
59            Parameter::optional(
60                "suffix",
61                kind::BYTES,
62                indoc! {"
63                    A custom suffix to be appended to truncated strings. If a custom `suffix` is
64                    provided, the total length of the string will be `limit + <suffix length>`.
65                "},
66            ),
67        ];
68        PARAMETERS
69    }
70
71    fn examples(&self) -> &'static [Example] {
72        &[
73            example! {
74                title: "Truncate a string",
75                source: r#"truncate("A rather long sentence.", limit: 11, suffix: "...")"#,
76                result: Ok("A rather lo..."),
77            },
78            example! {
79                title: "Truncate a string (custom suffix)",
80                source: r#"truncate("A rather long sentence.", limit: 11, suffix: "[TRUNCATED]")"#,
81                result: Ok("A rather lo[TRUNCATED]"),
82            },
83            example! {
84                title: "Truncate",
85                source: r#"truncate("foobar", 3)"#,
86                result: Ok("foo"),
87            },
88        ]
89    }
90
91    fn compile(
92        &self,
93        _state: &TypeState,
94        _ctx: &mut FunctionCompileContext,
95        arguments: ArgumentList,
96    ) -> Compiled {
97        let value = arguments.required("value");
98        let limit = arguments.required("limit");
99        let suffix = arguments.optional("suffix").unwrap_or(expr!(""));
100
101        Ok(TruncateFn {
102            value,
103            limit,
104            suffix,
105        }
106        .as_expr())
107    }
108}
109
110#[derive(Debug, Clone)]
111struct TruncateFn {
112    value: Box<dyn Expression>,
113    limit: Box<dyn Expression>,
114    suffix: Box<dyn Expression>,
115}
116
117impl FunctionExpression for TruncateFn {
118    fn resolve(&self, ctx: &mut Context) -> Resolved {
119        let value = self.value.resolve(ctx)?;
120        let limit = self.limit.resolve(ctx)?;
121        let suffix = self.suffix.resolve(ctx)?;
122
123        truncate(&value, limit, &suffix)
124    }
125
126    fn type_def(&self, _: &state::TypeState) -> TypeDef {
127        TypeDef::bytes().infallible()
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    test_function![
136        truncate => Truncate;
137
138        empty {
139             args: func_args![value: "Super",
140                              limit: 0,
141             ],
142             want: Ok(""),
143             tdef: TypeDef::bytes().infallible(),
144         }
145
146        ellipsis {
147            args: func_args![value: "Super",
148                             limit: 0,
149                             suffix: "..."
150            ],
151            want: Ok("..."),
152            tdef: TypeDef::bytes().infallible(),
153        }
154
155        complete {
156            args: func_args![value: "Super",
157                             limit: 10
158            ],
159            want: Ok("Super"),
160            tdef: TypeDef::bytes().infallible(),
161        }
162
163        exact {
164            args: func_args![value: "Super",
165                             limit: 5,
166                             suffix: "."
167            ],
168            want: Ok("Super"),
169            tdef: TypeDef::bytes().infallible(),
170        }
171
172        big {
173            args: func_args![value: "Supercalifragilisticexpialidocious",
174                             limit: 5
175            ],
176            want: Ok("Super"),
177            tdef: TypeDef::bytes().infallible(),
178        }
179
180        big_ellipsis {
181            args: func_args![value: "Supercalifragilisticexpialidocious",
182                             limit: 5,
183                             suffix: "..."
184            ],
185            want: Ok("Super..."),
186            tdef: TypeDef::bytes().infallible(),
187        }
188
189        unicode {
190            args: func_args![value: "♔♕♖♗♘♙♚♛♜♝♞♟",
191                             limit: 6,
192                             suffix: "..."
193            ],
194            want: Ok("♔♕♖♗♘♙..."),
195            tdef: TypeDef::bytes().infallible(),
196        }
197
198        alternative_suffix {
199            args: func_args![value: "Super",
200                             limit: 1,
201                             suffix: "[TRUNCATED]"
202            ],
203            want: Ok("S[TRUNCATED]"),
204            tdef: TypeDef::bytes().infallible(),
205        }
206    ];
207}