liquid_lib/stdlib/filters/
array.rs

1use std::cmp;
2
3use liquid_core::model::ValueViewCmp;
4use liquid_core::Expression;
5use liquid_core::Result;
6use liquid_core::Runtime;
7use liquid_core::{
8    Display_filter, Filter, FilterParameters, FilterReflection, FromFilterParameters, ParseFilter,
9};
10use liquid_core::{Value, ValueCow, ValueView};
11
12use crate::{invalid_argument, invalid_input};
13
14fn as_sequence<'k>(input: &'k dyn ValueView) -> Box<dyn Iterator<Item = &'k dyn ValueView> + 'k> {
15    if let Some(array) = input.as_array() {
16        array.values()
17    } else if input.is_nil() {
18        Box::new(vec![].into_iter())
19    } else {
20        Box::new(std::iter::once(input))
21    }
22}
23
24#[derive(Debug, FilterParameters)]
25struct JoinArgs {
26    #[parameter(
27        description = "The separator between each element in the string.",
28        arg_type = "str"
29    )]
30    separator: Option<Expression>,
31}
32
33#[derive(Clone, ParseFilter, FilterReflection)]
34#[filter(
35    name = "join",
36    description = "Combines the items in an array into a single string using the argument as a separator.",
37    parameters(JoinArgs),
38    parsed(JoinFilter)
39)]
40pub struct Join;
41
42#[derive(Debug, FromFilterParameters, Display_filter)]
43#[name = "join"]
44struct JoinFilter {
45    #[parameters]
46    args: JoinArgs,
47}
48
49impl Filter for JoinFilter {
50    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
51        let args = self.args.evaluate(runtime)?;
52
53        let separator = args.separator.unwrap_or_else(|| " ".into());
54
55        let input = input
56            .as_array()
57            .ok_or_else(|| invalid_input("Array of strings expected"))?;
58        let input = input.values().map(|x| x.to_kstr());
59
60        Ok(Value::scalar(itertools::join(input, separator.as_str())))
61    }
62}
63
64fn nil_safe_compare(a: &dyn ValueView, b: &dyn ValueView) -> Option<cmp::Ordering> {
65    if a.is_nil() && b.is_nil() {
66        Some(cmp::Ordering::Equal)
67    } else if a.is_nil() {
68        Some(cmp::Ordering::Greater)
69    } else if b.is_nil() {
70        Some(cmp::Ordering::Less)
71    } else {
72        ValueViewCmp::new(a).partial_cmp(&ValueViewCmp::new(b))
73    }
74}
75
76fn nil_safe_casecmp_key(value: &dyn ValueView) -> Option<String> {
77    if value.is_nil() {
78        None
79    } else {
80        Some(value.to_kstr().to_lowercase())
81    }
82}
83
84fn nil_safe_casecmp(a: &Option<String>, b: &Option<String>) -> Option<cmp::Ordering> {
85    match (a, b) {
86        (None, None) => Some(cmp::Ordering::Equal),
87        (None, _) => Some(cmp::Ordering::Greater),
88        (_, None) => Some(cmp::Ordering::Less),
89        (a, b) => a.partial_cmp(b),
90    }
91}
92
93#[derive(Debug, Default, FilterParameters)]
94struct PropertyArgs {
95    #[parameter(description = "The property accessed by the filter.", arg_type = "str")]
96    property: Option<Expression>,
97}
98
99#[derive(Clone, ParseFilter, FilterReflection)]
100#[filter(
101    name = "sort",
102    description = "Sorts items in an array. The order of the sorted array is case-sensitive.",
103    parameters(PropertyArgs),
104    parsed(SortFilter)
105)]
106pub struct Sort;
107
108#[derive(Debug, Default, FromFilterParameters, Display_filter)]
109#[name = "sort"]
110struct SortFilter {
111    #[parameters]
112    args: PropertyArgs,
113}
114
115fn safe_property_getter<'a>(value: &'a Value, property: &str) -> &'a dyn ValueView {
116    value
117        .as_object()
118        .and_then(|obj| obj.get(property))
119        .unwrap_or(&Value::Nil)
120}
121
122impl Filter for SortFilter {
123    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
124        let args = self.args.evaluate(runtime)?;
125
126        let input: Vec<_> = as_sequence(input).collect();
127        if args.property.is_some() && !input.iter().all(|v| v.is_object()) {
128            return Err(invalid_input("Array of objects expected"));
129        }
130
131        let mut sorted: Vec<Value> = input.iter().map(|v| v.to_value()).collect();
132        if let Some(property) = &args.property {
133            // Using unwrap is ok since all of the elements are objects
134            sorted.sort_by(|a, b| {
135                nil_safe_compare(
136                    safe_property_getter(a, property),
137                    safe_property_getter(b, property),
138                )
139                .unwrap_or(cmp::Ordering::Equal)
140            });
141        } else {
142            sorted.sort_by(|a, b| nil_safe_compare(a, b).unwrap_or(cmp::Ordering::Equal));
143        }
144        Ok(Value::array(sorted))
145    }
146}
147
148#[derive(Clone, ParseFilter, FilterReflection)]
149#[filter(
150    name = "sort_natural",
151    description = "Sorts items in an array.",
152    parameters(PropertyArgs),
153    parsed(SortNaturalFilter)
154)]
155pub struct SortNatural;
156
157#[derive(Debug, Default, FromFilterParameters, Display_filter)]
158#[name = "sort_natural"]
159struct SortNaturalFilter {
160    #[parameters]
161    args: PropertyArgs,
162}
163
164impl Filter for SortNaturalFilter {
165    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
166        let args = self.args.evaluate(runtime)?;
167
168        let input: Vec<_> = as_sequence(input).collect();
169        if args.property.is_some() && !input.iter().all(|v| v.is_object()) {
170            return Err(invalid_input("Array of objects expected"));
171        }
172
173        let mut sorted: Vec<_> = if let Some(property) = &args.property {
174            input
175                .iter()
176                .map(|v| v.to_value())
177                .map(|v| {
178                    (
179                        nil_safe_casecmp_key(&safe_property_getter(&v, property).to_value()),
180                        v,
181                    )
182                })
183                .collect()
184        } else {
185            input
186                .iter()
187                .map(|v| v.to_value())
188                .map(|v| (nil_safe_casecmp_key(&v), v))
189                .collect()
190        };
191        sorted.sort_by(|a, b| nil_safe_casecmp(&a.0, &b.0).unwrap_or(cmp::Ordering::Equal));
192        let result: Vec<_> = sorted.into_iter().map(|(_, v)| v).collect();
193        Ok(Value::array(result))
194    }
195}
196
197#[derive(Debug, FilterParameters)]
198struct WhereArgs {
199    #[parameter(description = "The property being matched", arg_type = "str")]
200    property: Expression,
201    #[parameter(
202        description = "The value the property is matched with",
203        arg_type = "any"
204    )]
205    target_value: Option<Expression>,
206}
207
208#[derive(Clone, ParseFilter, FilterReflection)]
209#[filter(
210    name = "where",
211    description = "Filter the elements of an array to those with a certain property value. \
212                   By default the target is any truthy value.",
213    parameters(WhereArgs),
214    parsed(WhereFilter)
215)]
216pub struct Where;
217
218#[derive(Debug, FromFilterParameters, Display_filter)]
219#[name = "where"]
220struct WhereFilter {
221    #[parameters]
222    args: WhereArgs,
223}
224
225impl Filter for WhereFilter {
226    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
227        let args = self.args.evaluate(runtime)?;
228        let property: &str = &args.property;
229        let target_value: Option<ValueCow<'_>> = args.target_value;
230
231        if let Some(array) = input.as_array() {
232            if !array.values().all(|v| v.is_object()) {
233                return Ok(Value::Nil);
234            }
235        } else if !input.is_object() {
236            return Err(invalid_input(
237                "Array of objects or a single object expected",
238            ));
239        }
240
241        let input = as_sequence(input);
242        let array: Vec<_> = match target_value {
243            None => input
244                .filter_map(|v| v.as_object())
245                .filter(|object| {
246                    object
247                        .get(property)
248                        .map(|v| v.query_state(liquid_core::model::State::Truthy))
249                        .unwrap_or(false)
250                })
251                .map(|object| object.to_value())
252                .collect(),
253            Some(target_value) => input
254                .filter_map(|v| v.as_object())
255                .filter(|object| {
256                    object
257                        .get(property)
258                        .map(|value| {
259                            let value = ValueViewCmp::new(value);
260                            target_value == value
261                        })
262                        .unwrap_or(false)
263                })
264                .map(|object| object.to_value())
265                .collect(),
266        };
267        Ok(Value::array(array))
268    }
269}
270
271/// Removes any duplicate elements in an array.
272///
273/// This has an O(n^2) worst-case complexity.
274#[derive(Clone, ParseFilter, FilterReflection)]
275#[filter(
276    name = "uniq",
277    description = "Removes any duplicate elements in an array.",
278    parsed(UniqFilter)
279)]
280pub struct Uniq;
281
282#[derive(Debug, Default, Display_filter)]
283#[name = "uniq"]
284struct UniqFilter;
285
286impl Filter for UniqFilter {
287    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
288        // TODO(#267) optional property parameter
289
290        let array = input
291            .as_array()
292            .ok_or_else(|| invalid_input("Array expected"))?;
293        let mut deduped: Vec<Value> = Vec::with_capacity(array.size() as usize);
294        for x in array.values() {
295            if !deduped
296                .iter()
297                .any(|v| ValueViewCmp::new(v.as_view()) == ValueViewCmp::new(x))
298            {
299                deduped.push(x.to_value());
300            }
301        }
302        Ok(Value::array(deduped))
303    }
304}
305
306#[derive(Clone, ParseFilter, FilterReflection)]
307#[filter(
308    name = "reverse",
309    description = "Reverses the order of the items in an array.",
310    parsed(ReverseFilter)
311)]
312pub struct Reverse;
313
314#[derive(Debug, Default, Display_filter)]
315#[name = "reverse"]
316struct ReverseFilter;
317
318impl Filter for ReverseFilter {
319    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
320        let mut array: Vec<_> = input
321            .as_array()
322            .ok_or_else(|| invalid_input("Array expected"))?
323            .values()
324            .map(|v| v.to_value())
325            .collect();
326        array.reverse();
327        Ok(Value::array(array))
328    }
329}
330
331#[derive(Debug, FilterParameters)]
332struct MapArgs {
333    #[parameter(
334        description = "The property to be extracted from the values in the input.",
335        arg_type = "str"
336    )]
337    property: Expression,
338}
339
340#[derive(Clone, ParseFilter, FilterReflection)]
341#[filter(
342    name = "map",
343    description = "Extract `property` from the `Value::Object` elements of an array.",
344    parameters(MapArgs),
345    parsed(MapFilter)
346)]
347pub struct Map;
348
349#[derive(Debug, FromFilterParameters, Display_filter)]
350#[name = "map"]
351struct MapFilter {
352    #[parameters]
353    args: MapArgs,
354}
355
356impl Filter for MapFilter {
357    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
358        let args = self.args.evaluate(runtime)?;
359
360        let array = input
361            .as_array()
362            .ok_or_else(|| invalid_input("Array expected"))?;
363
364        let result: Vec<_> = array
365            .values()
366            .filter_map(|v| {
367                v.as_object()
368                    .and_then(|v| v.get(&args.property))
369                    .map(|v| v.to_value())
370            })
371            .collect();
372        Ok(Value::array(result))
373    }
374}
375
376#[derive(Clone, ParseFilter, FilterReflection)]
377#[filter(
378    name = "compact",
379    description = "Remove nulls from an iterable.",
380    parameters(PropertyArgs),
381    parsed(CompactFilter)
382)]
383pub struct Compact;
384
385#[derive(Debug, Default, FromFilterParameters, Display_filter)]
386#[name = "compact"]
387struct CompactFilter {
388    #[parameters]
389    args: PropertyArgs,
390}
391
392impl Filter for CompactFilter {
393    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
394        let args = self.args.evaluate(runtime)?;
395
396        let array = input
397            .as_array()
398            .ok_or_else(|| invalid_input("Array expected"))?;
399
400        let result: Vec<_> = if let Some(property) = &args.property {
401            if !array.values().all(|v| v.is_object()) {
402                return Err(invalid_input("Array of objects expected"));
403            }
404            // Reject non objects that don't have the required property
405            array
406                .values()
407                .filter(|v| {
408                    !v.as_object()
409                        .and_then(|obj| obj.get(property.as_str()))
410                        .map(|v| v.is_nil())
411                        .unwrap_or(true)
412                })
413                .map(|v| v.to_value())
414                .collect()
415        } else {
416            array
417                .values()
418                .filter(|v| !v.is_nil())
419                .map(|v| v.to_value())
420                .collect()
421        };
422
423        Ok(Value::array(result))
424    }
425}
426
427#[derive(Debug, FilterParameters)]
428struct ConcatArgs {
429    #[parameter(description = "The array to concatenate the input with.")]
430    array: Expression,
431}
432
433#[derive(Clone, ParseFilter, FilterReflection)]
434#[filter(
435    name = "concat",
436    description = "Concatenates the input array with a given array.",
437    parameters(ConcatArgs),
438    parsed(ConcatFilter)
439)]
440pub struct Concat;
441
442#[derive(Debug, FromFilterParameters, Display_filter)]
443#[name = "concat"]
444struct ConcatFilter {
445    #[parameters]
446    args: ConcatArgs,
447}
448
449impl Filter for ConcatFilter {
450    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
451        let args = self.args.evaluate(runtime)?;
452
453        let input = input
454            .as_array()
455            .ok_or_else(|| invalid_input("Array expected"))?;
456        let input = input.values().map(|v| v.to_value());
457
458        let array = args
459            .array
460            .as_array()
461            .ok_or_else(|| invalid_argument("array", "Array expected"))?;
462        let array = array.values().map(|v| v.to_value());
463
464        let result = input.chain(array);
465        let result: Vec<_> = result.collect();
466        Ok(Value::array(result))
467    }
468}
469
470#[derive(Clone, ParseFilter, FilterReflection)]
471#[filter(
472    name = "first",
473    description = "Returns the first item of an array.",
474    parsed(FirstFilter)
475)]
476pub struct First;
477
478#[derive(Debug, Default, Display_filter)]
479#[name = "first"]
480struct FirstFilter;
481
482impl Filter for FirstFilter {
483    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
484        if let Some(x) = input.as_scalar() {
485            let c = x
486                .to_kstr()
487                .chars()
488                .next()
489                .map(|c| c.to_string())
490                .unwrap_or_else(|| "".to_owned());
491            Ok(Value::scalar(c))
492        } else if let Some(x) = input.as_array() {
493            Ok(x.first()
494                .map(|v| v.to_value())
495                .unwrap_or_else(|| Value::Nil))
496        } else {
497            Err(invalid_input("String or Array expected"))
498        }
499    }
500}
501
502#[derive(Clone, ParseFilter, FilterReflection)]
503#[filter(
504    name = "last",
505    description = "Returns the last item of an array.",
506    parsed(LastFilter)
507)]
508pub struct Last;
509
510#[derive(Debug, Default, Display_filter)]
511#[name = "last"]
512struct LastFilter;
513
514impl Filter for LastFilter {
515    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
516        if let Some(x) = input.as_scalar() {
517            let c = x
518                .to_kstr()
519                .chars()
520                .last()
521                .map(|c| c.to_string())
522                .unwrap_or_else(|| "".to_owned());
523            Ok(Value::scalar(c))
524        } else if let Some(x) = input.as_array() {
525            Ok(x.last().map(|v| v.to_value()).unwrap_or_else(|| Value::Nil))
526        } else {
527            Err(invalid_input("String or Array expected"))
528        }
529    }
530}
531
532#[cfg(test)]
533mod tests {
534
535    use super::*;
536
537    #[test]
538    fn unit_concat_nothing() {
539        let input = liquid_core::value!([1f64, 2f64]);
540        let result = liquid_core::value!([1f64, 2f64]);
541        assert_eq!(
542            liquid_core::call_filter!(Concat, input, liquid_core::value!([])).unwrap(),
543            result
544        );
545    }
546
547    #[test]
548    fn unit_concat_something() {
549        let input = liquid_core::value!([1f64, 2f64]);
550        let result = liquid_core::value!([1f64, 2f64, 3f64, 4f64]);
551        assert_eq!(
552            liquid_core::call_filter!(Concat, input, liquid_core::value!([3f64, 4f64])).unwrap(),
553            result
554        );
555    }
556
557    #[test]
558    fn unit_concat_mixed() {
559        let input = liquid_core::value!([1f64, 2f64]);
560        let result = liquid_core::value!([1f64, 2f64, 3f64, "a"]);
561        assert_eq!(
562            liquid_core::call_filter!(Concat, input, liquid_core::value!([3f64, "a"])).unwrap(),
563            result
564        );
565    }
566
567    #[test]
568    fn unit_concat_wrong_type() {
569        let input = liquid_core::value!([1f64, 2f64]);
570        liquid_core::call_filter!(Concat, input, 1f64).unwrap_err();
571    }
572
573    #[test]
574    fn unit_concat_no_args() {
575        let input = liquid_core::value!([1f64, 2f64]);
576        liquid_core::call_filter!(Concat, input).unwrap_err();
577    }
578
579    #[test]
580    fn unit_concat_extra_args() {
581        let input = liquid_core::value!([1f64, 2f64]);
582        liquid_core::call_filter!(Concat, input, liquid_core::value!([3f64, "a"]), 2f64)
583            .unwrap_err();
584    }
585
586    #[test]
587    #[allow(clippy::float_cmp)] // Need to dig into this
588    fn unit_first() {
589        assert_eq!(
590            liquid_core::call_filter!(First, liquid_core::value!([0f64, 1f64, 2f64, 3f64, 4f64,]))
591                .unwrap(),
592            0f64
593        );
594        assert_eq!(
595            liquid_core::call_filter!(First, liquid_core::value!(["test", "two"])).unwrap(),
596            liquid_core::value!("test")
597        );
598        assert_eq!(
599            liquid_core::call_filter!(First, liquid_core::value!([])).unwrap(),
600            Value::Nil
601        );
602    }
603
604    #[test]
605    fn unit_join() {
606        let input = liquid_core::value!(["a", "b", "c"]);
607        assert_eq!(
608            liquid_core::call_filter!(Join, input, ",").unwrap(),
609            liquid_core::value!("a,b,c")
610        );
611    }
612
613    #[test]
614    fn unit_join_bad_input() {
615        let input = "a";
616        liquid_core::call_filter!(Join, input, ",").unwrap_err();
617    }
618
619    #[test]
620    fn unit_join_bad_join_string() {
621        let input = liquid_core::value!(["a", "b", "c"]);
622        assert_eq!(
623            liquid_core::call_filter!(Join, input, 1f64).unwrap(),
624            "a1b1c"
625        );
626    }
627
628    #[test]
629    fn unit_join_no_args() {
630        let input = liquid_core::value!(["a", "b", "c"]);
631        assert_eq!(liquid_core::call_filter!(Join, input).unwrap(), "a b c");
632    }
633
634    #[test]
635    fn unit_join_non_string_element() {
636        let input = liquid_core::value!(["a", 1f64, "c"]);
637        assert_eq!(
638            liquid_core::call_filter!(Join, input, ",").unwrap(),
639            liquid_core::value!("a,1,c")
640        );
641    }
642
643    #[test]
644    fn unit_sort() {
645        let input = &liquid_core::value!(["Z", "b", "c", "a"]);
646        let desired_result = liquid_core::value!(["Z", "a", "b", "c"]);
647        assert_eq!(
648            liquid_core::call_filter!(Sort, input).unwrap(),
649            desired_result
650        );
651    }
652
653    #[test]
654    fn unit_sort_natural() {
655        let input = &liquid_core::value!(["Z", "b", "c", "a"]);
656        let desired_result = liquid_core::value!(["a", "b", "c", "Z"]);
657        assert_eq!(
658            liquid_core::call_filter!(SortNatural, input).unwrap(),
659            desired_result
660        );
661    }
662
663    #[test]
664    #[allow(clippy::float_cmp)] // Need to dig into this
665    fn unit_last() {
666        assert_eq!(
667            liquid_core::call_filter!(Last, liquid_core::value!([0f64, 1f64, 2f64, 3f64, 4f64,]))
668                .unwrap(),
669            4f64
670        );
671        assert_eq!(
672            liquid_core::call_filter!(Last, liquid_core::value!(["test", "last"])).unwrap(),
673            liquid_core::value!("last")
674        );
675        assert_eq!(
676            liquid_core::call_filter!(Last, liquid_core::value!([])).unwrap(),
677            Value::Nil
678        );
679    }
680
681    #[test]
682    fn unit_reverse_apples_oranges_peaches_plums() {
683        // First example from https://shopify.github.io/liquid/filters/reverse/
684        let input = liquid_core::value!(["apples", "oranges", "peaches", "plums"]);
685        let desired_result = liquid_core::value!(["plums", "peaches", "oranges", "apples"]);
686        assert_eq!(
687            liquid_core::call_filter!(Reverse, input).unwrap(),
688            desired_result
689        );
690    }
691
692    #[test]
693    fn unit_reverse_array() {
694        let input = liquid_core::value!([3f64, 1f64, 2f64]);
695        let desired_result = liquid_core::value!([2f64, 1f64, 3f64]);
696        assert_eq!(
697            liquid_core::call_filter!(Reverse, input).unwrap(),
698            desired_result
699        );
700    }
701
702    #[test]
703    fn unit_reverse_array_extra_args() {
704        let input = liquid_core::value!([3f64, 1f64, 2f64]);
705        liquid_core::call_filter!(Reverse, input, 0f64).unwrap_err();
706    }
707
708    #[test]
709    fn unit_reverse_ground_control_major_tom() {
710        // Second example from https://shopify.github.io/liquid/filters/reverse/
711        let input = liquid_core::value!([
712            "G", "r", "o", "u", "n", "d", " ", "c", "o", "n", "t", "r", "o", "l", " ", "t", "o",
713            " ", "M", "a", "j", "o", "r", " ", "T", "o", "m", ".",
714        ]);
715        let desired_result = liquid_core::value!([
716            ".", "m", "o", "T", " ", "r", "o", "j", "a", "M", " ", "o", "t", " ", "l", "o", "r",
717            "t", "n", "o", "c", " ", "d", "n", "u", "o", "r", "G",
718        ]);
719        assert_eq!(
720            liquid_core::call_filter!(Reverse, input).unwrap(),
721            desired_result
722        );
723    }
724
725    #[test]
726    fn unit_reverse_string() {
727        let input = "abc";
728        liquid_core::call_filter!(Reverse, input).unwrap_err();
729    }
730
731    #[test]
732    fn unit_uniq() {
733        let input = liquid_core::value!(["a", "b", "a"]);
734        let desired_result = liquid_core::value!(["a", "b"]);
735        assert_eq!(
736            liquid_core::call_filter!(Uniq, input).unwrap(),
737            desired_result
738        );
739    }
740
741    #[test]
742    fn unit_uniq_non_array() {
743        let input = 0f64;
744        liquid_core::call_filter!(Uniq, input).unwrap_err();
745    }
746
747    #[test]
748    fn unit_uniq_one_argument() {
749        let input = liquid_core::value!(["a", "b", "a"]);
750        liquid_core::call_filter!(Uniq, input, 0f64).unwrap_err();
751    }
752
753    #[test]
754    fn unit_uniq_shopify_liquid() {
755        // Test from https://shopify.github.io/liquid/filters/uniq/
756        let input = liquid_core::value!(["ants", "bugs", "bees", "bugs", "ants",]);
757        let desired_result = liquid_core::value!(["ants", "bugs", "bees"]);
758        assert_eq!(
759            liquid_core::call_filter!(Uniq, input).unwrap(),
760            desired_result
761        );
762    }
763}