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 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)] struct 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 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
234fn 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()], vec!["".into(), Value::Null, "".into()], 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 }, btreemap! {
346 "key1" => "",
347 "key2" => Value::Null,
348 "key3" => "",
349 }, 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}