vrl/stdlib/
compact.rs

1use super::util;
2use crate::compiler::prelude::*;
3use std::sync::LazyLock;
4
5static DEFAULT_RECURSIVE: LazyLock<Value> = LazyLock::new(|| Value::Boolean(true));
6static DEFAULT_NULL: LazyLock<Value> = LazyLock::new(|| Value::Boolean(true));
7static DEFAULT_STRING: LazyLock<Value> = LazyLock::new(|| Value::Boolean(true));
8static DEFAULT_OBJECT: LazyLock<Value> = LazyLock::new(|| Value::Boolean(true));
9static DEFAULT_ARRAY: LazyLock<Value> = LazyLock::new(|| Value::Boolean(true));
10static DEFAULT_NULLISH: LazyLock<Value> = LazyLock::new(|| Value::Boolean(false));
11
12static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
13    vec![
14        Parameter::required(
15            "value",
16            kind::OBJECT | kind::ARRAY,
17            "The object or array to compact.",
18        ),
19        Parameter::optional("recursive", kind::BOOLEAN, "Whether the compaction be recursive.")
20            .default(&DEFAULT_RECURSIVE),
21        Parameter::optional("null", kind::BOOLEAN, "Whether null should be treated as an empty value.")
22            .default(&DEFAULT_NULL),
23        Parameter::optional("string", kind::BOOLEAN, "Whether an empty string should be treated as an empty value.")
24            .default(&DEFAULT_STRING),
25        Parameter::optional("object", kind::BOOLEAN, "Whether an empty object should be treated as an empty value.")
26            .default(&DEFAULT_OBJECT),
27        Parameter::optional("array", kind::BOOLEAN, "Whether an empty array should be treated as an empty value.")
28            .default(&DEFAULT_ARRAY),
29        Parameter::optional("nullish", kind::BOOLEAN, "Tests whether the value is \"nullish\" as defined by the [`is_nullish`](#is_nullish) function.")
30            .default(&DEFAULT_NULLISH),
31    ]
32});
33
34fn compact(
35    recursive: Value,
36    null: Value,
37    string: Value,
38    object: Value,
39    array: Value,
40    nullish: Value,
41    value: Value,
42) -> Resolved {
43    let options = CompactOptions {
44        recursive: recursive.try_boolean()?,
45        null: null.try_boolean()?,
46        string: string.try_boolean()?,
47        object: object.try_boolean()?,
48        array: array.try_boolean()?,
49        nullish: nullish.try_boolean()?,
50    };
51
52    match value {
53        Value::Object(object) => Ok(Value::from(compact_object(object, &options))),
54        Value::Array(arr) => Ok(Value::from(compact_array(arr, &options))),
55        value => Err(ValueError::Expected {
56            got: value.kind(),
57            expected: Kind::array(Collection::any()) | Kind::object(Collection::any()),
58        }
59        .into()),
60    }
61}
62
63#[derive(Clone, Copy, Debug)]
64pub struct Compact;
65
66impl Function for Compact {
67    fn identifier(&self) -> &'static str {
68        "compact"
69    }
70
71    fn usage(&self) -> &'static str {
72        "Compacts the `value` by removing empty values, where empty values are defined using the available parameters."
73    }
74
75    fn category(&self) -> &'static str {
76        Category::Enumerate.as_ref()
77    }
78
79    fn return_kind(&self) -> u16 {
80        kind::ARRAY | kind::OBJECT
81    }
82
83    fn return_rules(&self) -> &'static [&'static str] {
84        &["The return type matches the `value` type."]
85    }
86
87    fn parameters(&self) -> &'static [Parameter] {
88        PARAMETERS.as_slice()
89    }
90
91    fn examples(&self) -> &'static [Example] {
92        &[
93            example! {
94                title: "Compact an object with default parameters",
95                source: r#"compact({"field1": 1, "field2": "", "field3": [], "field4": null})"#,
96                result: Ok(r#"{ "field1": 1 }"#),
97            },
98            example! {
99                title: "Compact an array with default parameters",
100                source: r#"compact(["foo", "bar", "", null, [], "buzz"])"#,
101                result: Ok(r#"["foo","bar","buzz"]"#),
102            },
103            example! {
104                title: "Compact an array using nullish",
105                source: r#"compact(["-", "   ", "\n", null, true], nullish: true)"#,
106                result: Ok("[true]"),
107            },
108            example! {
109                title: "Compact a complex object with default parameters",
110                source: r#"compact({ "a": {}, "b": null, "c": [null], "d": "", "e": "-", "f": true })"#,
111                result: Ok(r#"{ "e": "-", "f": true }"#),
112            },
113            example! {
114                title: "Compact a complex object using null: false",
115                source: r#"compact({ "a": {}, "b": null, "c": [null], "d": "", "e": "-", "f": true }, null: false)"#,
116                result: Ok(r#"{ "b": null, "c": [null], "e": "-", "f": true }"#),
117            },
118        ]
119    }
120
121    fn compile(
122        &self,
123        _state: &state::TypeState,
124        _ctx: &mut FunctionCompileContext,
125        arguments: ArgumentList,
126    ) -> Compiled {
127        let value = arguments.required("value");
128        let recursive = arguments.optional("recursive");
129        let null = arguments.optional("null");
130        let string = arguments.optional("string");
131        let object = arguments.optional("object");
132        let array = arguments.optional("array");
133        let nullish = arguments.optional("nullish");
134
135        Ok(CompactFn {
136            value,
137            recursive,
138            null,
139            string,
140            object,
141            array,
142            nullish,
143        }
144        .as_expr())
145    }
146}
147
148#[derive(Debug, Clone)]
149struct CompactFn {
150    value: Box<dyn Expression>,
151    recursive: Option<Box<dyn Expression>>,
152    null: Option<Box<dyn Expression>>,
153    string: Option<Box<dyn Expression>>,
154    object: Option<Box<dyn Expression>>,
155    array: Option<Box<dyn Expression>>,
156    nullish: Option<Box<dyn Expression>>,
157}
158
159#[derive(Debug)]
160#[allow(clippy::struct_excessive_bools)] // TODO replace with bitflags
161struct CompactOptions {
162    recursive: bool,
163    null: bool,
164    string: bool,
165    object: bool,
166    array: bool,
167    nullish: bool,
168}
169
170impl Default for CompactOptions {
171    fn default() -> Self {
172        Self {
173            recursive: true,
174            null: true,
175            string: true,
176            object: true,
177            array: true,
178            nullish: false,
179        }
180    }
181}
182
183impl CompactOptions {
184    /// Check if the value is empty according to the given options
185    fn is_empty(&self, value: &Value) -> bool {
186        if self.nullish && util::is_nullish(value) {
187            return true;
188        }
189
190        match value {
191            Value::Bytes(bytes) => self.string && bytes.len() == 0,
192            Value::Null => self.null,
193            Value::Object(object) => self.object && object.is_empty(),
194            Value::Array(array) => self.array && array.is_empty(),
195            _ => false,
196        }
197    }
198}
199
200impl FunctionExpression for CompactFn {
201    fn resolve(&self, ctx: &mut Context) -> Resolved {
202        let recursive = self
203            .recursive
204            .map_resolve_with_default(ctx, || DEFAULT_RECURSIVE.clone())?;
205        let null = self
206            .null
207            .map_resolve_with_default(ctx, || DEFAULT_NULL.clone())?;
208        let string = self
209            .string
210            .map_resolve_with_default(ctx, || DEFAULT_STRING.clone())?;
211        let object = self
212            .object
213            .map_resolve_with_default(ctx, || DEFAULT_OBJECT.clone())?;
214        let array = self
215            .array
216            .map_resolve_with_default(ctx, || DEFAULT_ARRAY.clone())?;
217        let nullish = self
218            .nullish
219            .map_resolve_with_default(ctx, || DEFAULT_NULLISH.clone())?;
220        let value = self.value.resolve(ctx)?;
221
222        compact(recursive, null, string, object, array, nullish, value)
223    }
224
225    fn type_def(&self, state: &state::TypeState) -> TypeDef {
226        if self.value.type_def(state).is_array() {
227            TypeDef::array(Collection::any())
228        } else {
229            TypeDef::object(Collection::any())
230        }
231    }
232}
233
234/// Compact the value if we are recursing - otherwise, just return the value untouched.
235fn recurse_compact(value: Value, options: &CompactOptions) -> Value {
236    match value {
237        Value::Array(array) if options.recursive => Value::from(compact_array(array, options)),
238        Value::Object(object) if options.recursive => Value::from(compact_object(object, options)),
239        _ => value,
240    }
241}
242
243fn compact_object(object: ObjectMap, options: &CompactOptions) -> ObjectMap {
244    object
245        .into_iter()
246        .filter_map(|(key, value)| {
247            let value = recurse_compact(value, options);
248            if options.is_empty(&value) {
249                None
250            } else {
251                Some((key, value))
252            }
253        })
254        .collect()
255}
256
257fn compact_array(array: Vec<Value>, options: &CompactOptions) -> Vec<Value> {
258    array
259        .into_iter()
260        .filter_map(|value| {
261            let value = recurse_compact(value, options);
262            if options.is_empty(&value) {
263                None
264            } else {
265                Some(value)
266            }
267        })
268        .collect()
269}
270
271#[cfg(test)]
272mod test {
273    use super::*;
274    use crate::btreemap;
275
276    #[test]
277    fn test_compacted_array() {
278        let cases = vec![
279            (
280                vec!["".into(), "".into()],              // expected
281                vec!["".into(), Value::Null, "".into()], // original
282                CompactOptions {
283                    string: false,
284                    ..CompactOptions::default()
285                },
286            ),
287            (
288                vec![1.into(), 2.into()],
289                vec![1.into(), Value::Array(vec![]), 2.into()],
290                CompactOptions::default(),
291            ),
292            (
293                vec![1.into(), Value::Array(vec![3.into()]), 2.into()],
294                vec![
295                    1.into(),
296                    Value::Array(vec![Value::Null, 3.into(), Value::Null]),
297                    2.into(),
298                ],
299                CompactOptions::default(),
300            ),
301            (
302                vec![1.into(), 2.into()],
303                vec![
304                    1.into(),
305                    Value::Array(vec![Value::Null, Value::Null]),
306                    2.into(),
307                ],
308                CompactOptions::default(),
309            ),
310            (
311                vec![
312                    Value::from(1),
313                    Value::Object(ObjectMap::from([(
314                        KeyString::from("field2"),
315                        Value::from(2),
316                    )])),
317                    Value::from(2),
318                ],
319                vec![
320                    1.into(),
321                    Value::Object(ObjectMap::from([
322                        (KeyString::from("field1"), Value::Null),
323                        (KeyString::from("field2"), Value::from(2)),
324                    ])),
325                    2.into(),
326                ],
327                CompactOptions::default(),
328            ),
329        ];
330
331        for (expected, original, options) in cases {
332            assert_eq!(expected, compact_array(original, &options));
333        }
334    }
335
336    #[test]
337    #[allow(clippy::too_many_lines)]
338    fn test_compacted_map() {
339        let cases = vec![
340            (
341                btreemap! {
342                    "key1" => "",
343                    "key3" => "",
344                }, // expected
345                btreemap! {
346                    "key1" => "",
347                    "key2" => Value::Null,
348                    "key3" => "",
349                }, // original
350                CompactOptions {
351                    string: false,
352                    ..CompactOptions::default()
353                },
354            ),
355            (
356                btreemap! {
357                    "key1" => Value::from(1),
358                    "key3" => Value::from(2),
359                },
360                btreemap! {
361                    "key1" => Value::from(1),
362                    "key2" => Value::Array(vec![]),
363                    "key3" => Value::from(2),
364                },
365                CompactOptions::default(),
366            ),
367            (
368                ObjectMap::from([
369                    (KeyString::from("key1"), Value::from(1)),
370                    (
371                        KeyString::from("key2"),
372                        Value::Object(ObjectMap::from([(KeyString::from("key2"), Value::from(3))])),
373                    ),
374                    (KeyString::from("key3"), Value::from(2)),
375                ]),
376                ObjectMap::from([
377                    (KeyString::from("key1"), Value::from(1)),
378                    (
379                        KeyString::from("key2"),
380                        Value::Object(ObjectMap::from([
381                            (KeyString::from("key1"), Value::Null),
382                            (KeyString::from("key2"), Value::from(3)),
383                            (KeyString::from("key3"), Value::Null),
384                        ])),
385                    ),
386                    (KeyString::from("key3"), Value::from(2)),
387                ]),
388                CompactOptions::default(),
389            ),
390            (
391                ObjectMap::from([
392                    (KeyString::from("key1"), Value::from(1)),
393                    (
394                        KeyString::from("key2"),
395                        Value::Object(ObjectMap::from([(KeyString::from("key1"), Value::Null)])),
396                    ),
397                    (KeyString::from("key3"), Value::from(2)),
398                ]),
399                ObjectMap::from([
400                    (KeyString::from("key1"), Value::from(1)),
401                    (
402                        KeyString::from("key2"),
403                        Value::Object(ObjectMap::from([(KeyString::from("key1"), Value::Null)])),
404                    ),
405                    (KeyString::from("key3"), Value::from(2)),
406                ]),
407                CompactOptions {
408                    recursive: false,
409                    ..CompactOptions::default()
410                },
411            ),
412            (
413                ObjectMap::from([
414                    (KeyString::from("key1"), Value::from(1)),
415                    (KeyString::from("key3"), Value::from(2)),
416                ]),
417                ObjectMap::from([
418                    (KeyString::from("key1"), Value::from(1)),
419                    (
420                        KeyString::from("key2"),
421                        Value::Object(ObjectMap::from([(KeyString::from("key1"), Value::Null)])),
422                    ),
423                    (KeyString::from("key3"), Value::from(2)),
424                ]),
425                CompactOptions::default(),
426            ),
427            (
428                btreemap! {
429                    "key1" => Value::from(1),
430                    "key2" => Value::Array(vec![2.into()]),
431                    "key3" => Value::from(2),
432                },
433                btreemap! {
434                    "key1" => Value::from(1),
435                    "key2" => Value::Array(vec![Value::Null, 2.into(), Value::Null]),
436                    "key3" => Value::from(2),
437                },
438                CompactOptions::default(),
439            ),
440        ];
441
442        for (expected, original, options) in cases {
443            assert_eq!(expected, compact_object(original, &options));
444        }
445    }
446
447    test_function![
448        compact => Compact;
449
450        with_map {
451            args: func_args![value: Value::from(ObjectMap::from([(KeyString::from("key1"), Value::Null), (KeyString::from("key2"), Value::from(1)), (KeyString::from("key3"), Value::from(""))]))],
452            want: Ok(Value::Object(ObjectMap::from([(KeyString::from("key2"), Value::from(1))]))),
453            tdef: TypeDef::object(Collection::any()),
454        }
455
456        with_array {
457            args: func_args![value: vec![Value::Null, Value::from(1), Value::from(""),]],
458            want: Ok(Value::Array(vec![Value::from(1)])),
459            tdef: TypeDef::array(Collection::any()),
460        }
461
462        nullish {
463            args: func_args![
464                value: btreemap! {
465                    "key1" => "-",
466                    "key2" => 1,
467                    "key3" => " "
468                },
469                nullish: true
470            ],
471            want: Ok(Value::Object(ObjectMap::from([(KeyString::from("key2"), Value::from(1))]))),
472            tdef: TypeDef::object(Collection::any()),
473        }
474    ];
475}