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