vrl/stdlib/
remove.rs

1use crate::compiler::prelude::*;
2use crate::path::{OwnedSegment, OwnedValuePath};
3use std::sync::LazyLock;
4
5static DEFAULT_COMPACT: LazyLock<Value> = LazyLock::new(|| Value::Boolean(false));
6
7static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
8    vec![
9        Parameter::required(
10            "value",
11            kind::OBJECT | kind::ARRAY,
12            "The object or array to remove data from.",
13        ),
14        Parameter::required(
15            "path",
16            kind::ARRAY,
17            "An array of path segments to remove the value from.",
18        ),
19        Parameter::optional(
20            "compact",
21            kind::BOOLEAN,
22            "After deletion, if `compact` is `true`, any empty objects or
23arrays left are also removed.",
24        )
25        .default(&DEFAULT_COMPACT),
26    ]
27});
28
29fn remove(path: Value, compact: Value, mut value: Value) -> Resolved {
30    let path = match path {
31        Value::Array(path) => {
32            let mut lookup = OwnedValuePath::root();
33
34            for segment in path {
35                let segment = match segment {
36                    Value::Bytes(field) => {
37                        OwnedSegment::Field(String::from_utf8_lossy(&field).into())
38                    }
39                    #[allow(clippy::cast_possible_truncation)] //TODO evaluate removal options
40                    Value::Integer(index) => OwnedSegment::Index(index as isize),
41                    value => {
42                        return Err(format!(
43                            "path segment must be either string or integer, not {}",
44                            value.kind()
45                        )
46                        .into());
47                    }
48                };
49
50                lookup.segments.push(segment);
51            }
52
53            lookup
54        }
55        value => {
56            return Err(ValueError::Expected {
57                got: value.kind(),
58                expected: Kind::array(Collection::any()),
59            }
60            .into());
61        }
62    };
63    let compact = compact.try_boolean()?;
64    value.remove(&path, compact);
65    Ok(value)
66}
67
68#[derive(Clone, Copy, Debug)]
69pub struct Remove;
70
71impl Function for Remove {
72    fn identifier(&self) -> &'static str {
73        "remove"
74    }
75
76    fn usage(&self) -> &'static str {
77        indoc! {"
78            Dynamically remove the value for a given path.
79
80            If you know the path you want to remove, use
81            the `del` function and static paths such as `del(.foo.bar[1])`
82            to remove the value at that path. The `del` function returns the
83            deleted value, and is more performant than `remove`.
84            However, if you do not know the path names, use the dynamic
85            `remove` function to remove the value at the provided path.
86        "}
87    }
88
89    fn category(&self) -> &'static str {
90        Category::Path.as_ref()
91    }
92
93    fn internal_failure_reasons(&self) -> &'static [&'static str] {
94        &["The `path` segment must be a string or an integer."]
95    }
96
97    fn return_kind(&self) -> u16 {
98        kind::OBJECT | kind::ARRAY
99    }
100
101    fn parameters(&self) -> &'static [Parameter] {
102        PARAMETERS.as_slice()
103    }
104
105    fn examples(&self) -> &'static [Example] {
106        &[
107            example! {
108                title: "Single-segment top-level field",
109                source: r#"remove!(value: { "foo": "bar" }, path: ["foo"])"#,
110                result: Ok("{}"),
111            },
112            example! {
113                title: "Remove unknown field",
114                source: r#"remove!(value: {"foo": "bar"}, path: ["baz"])"#,
115                result: Ok(r#"{ "foo": "bar" }"#),
116            },
117            example! {
118                title: "Multi-segment nested field",
119                source: r#"remove!(value: { "foo": { "bar": "baz" } }, path: ["foo", "bar"])"#,
120                result: Ok(r#"{ "foo": {} }"#),
121            },
122            example! {
123                title: "Array indexing",
124                source: r#"remove!(value: ["foo", "bar", "baz"], path: [-2])"#,
125                result: Ok(r#"["foo", "baz"]"#),
126            },
127            example! {
128                title: "Compaction",
129                source: r#"remove!(value: { "foo": { "bar": [42], "baz": true } }, path: ["foo", "bar", 0], compact: true)"#,
130                result: Ok(r#"{ "foo": { "baz": true } }"#),
131            },
132            example! {
133                title: "Compact object",
134                source: r#"remove!(value: {"foo": { "bar": true }}, path: ["foo", "bar"], compact: true)"#,
135                result: Ok("{}"),
136            },
137            example! {
138                title: "Compact array",
139                source: r#"remove!(value: {"foo": [42], "bar": true }, path: ["foo", 0], compact: true)"#,
140                result: Ok(r#"{ "bar": true }"#),
141            },
142            example! {
143                title: "External target",
144                source: r#"remove!(value: ., path: ["foo"])"#,
145                input: r#"{ "foo": true }"#,
146                result: Ok("{}"),
147            },
148            example! {
149                title: "Variable",
150                source: indoc! {r#"
151                    var = { "foo": true }
152                    remove!(value: var, path: ["foo"])
153                "#},
154                result: Ok("{}"),
155            },
156            example! {
157                title: "Missing index",
158                source: r#"remove!(value: {"foo": { "bar": [92, 42] }}, path: ["foo", "bar", 1, -1])"#,
159                result: Ok(r#"{ "foo": { "bar": [92, 42] } }"#),
160            },
161            example! {
162                title: "Invalid indexing",
163                source: r#"remove!(value: [42], path: ["foo"])"#,
164                result: Ok("[42]"),
165            },
166            example! {
167                title: "Invalid segment type",
168                source: r#"remove!(value: {"foo": { "bar": [92, 42] }}, path: ["foo", true])"#,
169                result: Err(
170                    r#"function call error for "remove" at (0:65): path segment must be either string or integer, not boolean"#,
171                ),
172            },
173        ]
174    }
175
176    fn compile(
177        &self,
178        _state: &state::TypeState,
179        _ctx: &mut FunctionCompileContext,
180        arguments: ArgumentList,
181    ) -> Compiled {
182        let value = arguments.required("value");
183        let path = arguments.required("path");
184        let compact = arguments.optional("compact");
185
186        Ok(RemoveFn {
187            value,
188            path,
189            compact,
190        }
191        .as_expr())
192    }
193}
194
195#[derive(Debug, Clone)]
196pub(crate) struct RemoveFn {
197    value: Box<dyn Expression>,
198    path: Box<dyn Expression>,
199    compact: Option<Box<dyn Expression>>,
200}
201
202impl FunctionExpression for RemoveFn {
203    fn resolve(&self, ctx: &mut Context) -> Resolved {
204        let path = self.path.resolve(ctx)?;
205        let compact = self
206            .compact
207            .map_resolve_with_default(ctx, || DEFAULT_COMPACT.clone())?;
208        let value = self.value.resolve(ctx)?;
209
210        remove(path, compact, value)
211    }
212
213    fn type_def(&self, state: &state::TypeState) -> TypeDef {
214        let value_td = self.value.type_def(state);
215
216        let mut td = TypeDef::from(Kind::never()).fallible();
217
218        if value_td.is_array() {
219            td = td.or_array(Collection::any());
220        }
221
222        if value_td.is_object() {
223            td = td.or_object(Collection::any());
224        }
225
226        td
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::value;
234
235    test_function![
236        remove => Remove;
237
238        array {
239            args: func_args![value: value!([42]), path: value!([0])],
240            want: Ok(value!([])),
241            tdef: TypeDef::array(Collection::any()).fallible(),
242        }
243
244        object {
245            args: func_args![value: value!({ "foo": 42 }), path: value!(["foo"])],
246            want: Ok(value!({})),
247            tdef: TypeDef::object(Collection::any()).fallible(),
248        }
249    ];
250}