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)] 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}