• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cobalt-org / liquid-rust / 13123086265

03 Feb 2025 09:21PM UTC coverage: 53.213%. First build
13123086265

Pull #576

github

web-flow
Merge 9a9b32422 into be5b50b7b
Pull Request #576: chore: Update from _rust template

19 of 34 new or added lines in 14 files covered. (55.88%)

2559 of 4809 relevant lines covered (53.21%)

4.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.02
/crates/lib/src/stdlib/filters/array.rs
1
use std::cmp;
2

3
use liquid_core::model::ValueViewCmp;
4
use liquid_core::Expression;
5
use liquid_core::Result;
6
use liquid_core::Runtime;
7
use liquid_core::{
8
    Display_filter, Filter, FilterParameters, FilterReflection, FromFilterParameters, ParseFilter,
9
};
10
use liquid_core::{Value, ValueCow, ValueView};
11

12
use crate::{invalid_argument, invalid_input};
13

14
fn as_sequence<'k>(input: &'k dyn ValueView) -> Box<dyn Iterator<Item = &'k dyn ValueView> + 'k> {
2✔
15
    if let Some(array) = input.as_array() {
7✔
16
        array.values()
2✔
17
    } else if input.is_nil() {
2✔
18
        Box::new(vec![].into_iter())
×
19
    } else {
20
        Box::new(std::iter::once(input))
2✔
21
    }
22
}
23

24
#[derive(Debug, FilterParameters)]
25
struct 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
)]
40
pub struct Join;
41

42
#[derive(Debug, FromFilterParameters, Display_filter)]
43
#[name = "join"]
44
struct JoinFilter {
45
    #[parameters]
46
    args: JoinArgs,
47
}
48

49
impl Filter for JoinFilter {
50
    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
2✔
51
        let args = self.args.evaluate(runtime)?;
2✔
52

53
        let separator = args.separator.unwrap_or_else(|| " ".into());
6✔
54

55
        let input = input
4✔
56
            .as_array()
57
            .ok_or_else(|| invalid_input("Array of strings expected"))?;
×
58
        let input = input.values().map(|x| x.to_kstr());
8✔
59

60
        Ok(Value::scalar(itertools::join(input, separator.as_str())))
2✔
61
    }
62
}
63

64
fn nil_safe_compare(a: &dyn ValueView, b: &dyn ValueView) -> Option<cmp::Ordering> {
2✔
65
    if a.is_nil() && b.is_nil() {
4✔
66
        Some(cmp::Ordering::Equal)
1✔
67
    } else if a.is_nil() {
3✔
68
        Some(cmp::Ordering::Greater)
1✔
69
    } else if b.is_nil() {
2✔
70
        Some(cmp::Ordering::Less)
1✔
71
    } else {
72
        ValueViewCmp::new(a).partial_cmp(&ValueViewCmp::new(b))
2✔
73
    }
74
}
75

76
fn nil_safe_casecmp_key(value: &dyn ValueView) -> Option<String> {
1✔
77
    if value.is_nil() {
2✔
78
        None
1✔
79
    } else {
80
        Some(value.to_kstr().to_lowercase())
2✔
81
    }
82
}
83

84
fn nil_safe_casecmp(a: &Option<String>, b: &Option<String>) -> Option<cmp::Ordering> {
1✔
85
    match (a, b) {
1✔
86
        (None, None) => Some(cmp::Ordering::Equal),
1✔
87
        (None, _) => Some(cmp::Ordering::Greater),
1✔
88
        (_, None) => Some(cmp::Ordering::Less),
1✔
89
        (a, b) => a.partial_cmp(b),
1✔
90
    }
91
}
92

93
#[derive(Debug, Default, FilterParameters)]
94
struct 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
)]
106
pub struct Sort;
107

108
#[derive(Debug, Default, FromFilterParameters, Display_filter)]
109
#[name = "sort"]
110
struct SortFilter {
111
    #[parameters]
112
    args: PropertyArgs,
113
}
114

115
fn safe_property_getter<'a>(value: &'a Value, property: &str) -> &'a dyn ValueView {
1✔
116
    value
1✔
117
        .as_object()
118
        .and_then(|obj| obj.get(property))
2✔
119
        .unwrap_or(&Value::Nil)
×
120
}
121

122
impl Filter for SortFilter {
123
    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
2✔
124
        let args = self.args.evaluate(runtime)?;
2✔
125

126
        let input: Vec<_> = as_sequence(input).collect();
4✔
127
        if args.property.is_some() && !input.iter().all(|v| v.is_object()) {
7✔
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();
8✔
132
        if let Some(property) = &args.property {
2✔
133
            // Using unwrap is ok since all of the elements are objects
134
            sorted.sort_by(|a, b| {
3✔
135
                nil_safe_compare(
2✔
136
                    safe_property_getter(a, property),
1✔
137
                    safe_property_getter(b, property),
1✔
138
                )
139
                .unwrap_or(cmp::Ordering::Equal)
1✔
140
            });
141
        } else {
142
            sorted.sort_by(|a, b| nil_safe_compare(a, b).unwrap_or(cmp::Ordering::Equal));
8✔
143
        }
144
        Ok(Value::array(sorted))
4✔
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
)]
155
pub struct SortNatural;
156

157
#[derive(Debug, Default, FromFilterParameters, Display_filter)]
158
#[name = "sort_natural"]
159
struct SortNaturalFilter {
160
    #[parameters]
161
    args: PropertyArgs,
162
}
163

164
impl Filter for SortNaturalFilter {
165
    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
1✔
166
        let args = self.args.evaluate(runtime)?;
1✔
167

168
        let input: Vec<_> = as_sequence(input).collect();
2✔
169
        if args.property.is_some() && !input.iter().all(|v| v.is_object()) {
5✔
170
            return Err(invalid_input("Array of objects expected"));
×
171
        }
172

173
        let mut sorted: Vec<_> = if let Some(property) = &args.property {
3✔
174
            input
2✔
175
                .iter()
176
                .map(|v| v.to_value())
2✔
177
                .map(|v| {
1✔
178
                    (
179
                        nil_safe_casecmp_key(&safe_property_getter(&v, property).to_value()),
2✔
180
                        v,
1✔
181
                    )
182
                })
183
                .collect()
184
        } else {
185
            input
2✔
186
                .iter()
187
                .map(|v| v.to_value())
2✔
188
                .map(|v| (nil_safe_casecmp_key(&v), v))
2✔
189
                .collect()
190
        };
191
        sorted.sort_by(|a, b| nil_safe_casecmp(&a.0, &b.0).unwrap_or(cmp::Ordering::Equal));
4✔
192
        let result: Vec<_> = sorted.into_iter().map(|(_, v)| v).collect();
3✔
193
        Ok(Value::array(result))
1✔
194
    }
195
}
196

197
#[derive(Debug, FilterParameters)]
198
struct 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
)]
216
pub struct Where;
217

218
#[derive(Debug, FromFilterParameters, Display_filter)]
219
#[name = "where"]
220
struct WhereFilter {
221
    #[parameters]
222
    args: WhereArgs,
223
}
224

225
impl Filter for WhereFilter {
226
    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
1✔
227
        let args = self.args.evaluate(runtime)?;
1✔
228
        let property: &str = &args.property;
4✔
229
        let target_value: Option<ValueCow<'_>> = args.target_value;
3✔
230

231
        if let Some(array) = input.as_array() {
6✔
232
            if !array.values().all(|v| v.is_object()) {
7✔
233
                return Ok(Value::Nil);
1✔
234
            }
235
        } else if !input.is_object() {
2✔
236
            return Err(invalid_input(
1✔
237
                "Array of objects or a single object expected",
238
            ));
239
        }
240

241
        let input = as_sequence(input);
2✔
242
        let array: Vec<_> = match target_value {
1✔
243
            None => input
2✔
244
                .filter_map(|v| v.as_object())
2✔
245
                .filter(|object| {
1✔
246
                    object
2✔
247
                        .get(property)
1✔
248
                        .map(|v| v.query_state(liquid_core::model::State::Truthy))
2✔
249
                        .unwrap_or(false)
250
                })
251
                .map(|object| object.to_value())
3✔
252
                .collect(),
253
            Some(target_value) => input
2✔
254
                .filter_map(|v| v.as_object())
3✔
255
                .filter(|object| {
3✔
256
                    object
3✔
257
                        .get(property)
2✔
258
                        .map(|value| {
4✔
259
                            let value = ValueViewCmp::new(value);
1✔
260
                            target_value == value
2✔
261
                        })
262
                        .unwrap_or(false)
263
                })
264
                .map(|object| object.to_value())
3✔
265
                .collect(),
266
        };
267
        Ok(Value::array(array))
3✔
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
)]
280
pub struct Uniq;
281

282
#[derive(Debug, Default, Display_filter)]
283
#[name = "uniq"]
284
struct UniqFilter;
285

286
impl Filter for UniqFilter {
287
    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
1✔
288
        // TODO(#267) optional property parameter
289

290
        let array = input
2✔
291
            .as_array()
292
            .ok_or_else(|| invalid_input("Array expected"))?;
3✔
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
            {
NEW
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
)]
312
pub struct Reverse;
313

314
#[derive(Debug, Default, Display_filter)]
315
#[name = "reverse"]
316
struct ReverseFilter;
317

318
impl Filter for ReverseFilter {
319
    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
1✔
320
        let mut array: Vec<_> = input
2✔
321
            .as_array()
322
            .ok_or_else(|| invalid_input("Array expected"))?
3✔
323
            .values()
324
            .map(|v| v.to_value())
2✔
325
            .collect();
326
        array.reverse();
2✔
327
        Ok(Value::array(array))
1✔
328
    }
329
}
330

331
#[derive(Debug, FilterParameters)]
332
struct 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
)]
347
pub struct Map;
348

349
#[derive(Debug, FromFilterParameters, Display_filter)]
350
#[name = "map"]
351
struct MapFilter {
352
    #[parameters]
353
    args: MapArgs,
354
}
355

356
impl Filter for MapFilter {
357
    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
2✔
358
        let args = self.args.evaluate(runtime)?;
2✔
359

360
        let array = input
5✔
361
            .as_array()
362
            .ok_or_else(|| invalid_input("Array expected"))?;
3✔
363

364
        let result: Vec<_> = array
4✔
365
            .values()
366
            .filter_map(|v| {
2✔
367
                v.as_object()
4✔
368
                    .and_then(|v| v.get(&args.property))
6✔
369
                    .map(|v| v.to_value())
4✔
370
            })
371
            .collect();
372
        Ok(Value::array(result))
2✔
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
)]
383
pub struct Compact;
384

385
#[derive(Debug, Default, FromFilterParameters, Display_filter)]
386
#[name = "compact"]
387
struct CompactFilter {
388
    #[parameters]
389
    args: PropertyArgs,
390
}
391

392
impl Filter for CompactFilter {
393
    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
2✔
394
        let args = self.args.evaluate(runtime)?;
2✔
395

396
        let array = input
4✔
397
            .as_array()
398
            .ok_or_else(|| invalid_input("Array expected"))?;
×
399

400
        let result: Vec<_> = if let Some(property) = &args.property {
4✔
401
            if !array.values().all(|v| v.is_object()) {
8✔
402
                return Err(invalid_input("Array of objects expected"));
1✔
403
            }
404
            // Reject non objects that don't have the required property
405
            array
2✔
406
                .values()
407
                .filter(|v| {
2✔
408
                    !v.as_object()
4✔
409
                        .and_then(|obj| obj.get(property.as_str()))
6✔
410
                        .map(|v| v.is_nil())
4✔
411
                        .unwrap_or(true)
412
                })
413
                .map(|v| v.to_value())
4✔
414
                .collect()
415
        } else {
416
            array
2✔
417
                .values()
418
                .filter(|v| !v.is_nil())
2✔
419
                .map(|v| v.to_value())
2✔
420
                .collect()
421
        };
422

423
        Ok(Value::array(result))
4✔
424
    }
425
}
426

427
#[derive(Debug, FilterParameters)]
428
struct 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
)]
440
pub struct Concat;
441

442
#[derive(Debug, FromFilterParameters, Display_filter)]
443
#[name = "concat"]
444
struct ConcatFilter {
445
    #[parameters]
446
    args: ConcatArgs,
447
}
448

449
impl Filter for ConcatFilter {
450
    fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
1✔
451
        let args = self.args.evaluate(runtime)?;
1✔
452

453
        let input = input
2✔
454
            .as_array()
455
            .ok_or_else(|| invalid_input("Array expected"))?;
×
456
        let input = input.values().map(|v| v.to_value());
4✔
457

458
        let array = args
3✔
459
            .array
460
            .as_array()
461
            .ok_or_else(|| invalid_argument("array", "Array expected"))?;
3✔
462
        let array = array.values().map(|v| v.to_value());
4✔
463

464
        let result = input.chain(array);
1✔
465
        let result: Vec<_> = result.collect();
1✔
466
        Ok(Value::array(result))
1✔
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
)]
476
pub struct First;
477

478
#[derive(Debug, Default, Display_filter)]
479
#[name = "first"]
480
struct FirstFilter;
481

482
impl Filter for FirstFilter {
483
    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
2✔
484
        if let Some(x) = input.as_scalar() {
2✔
485
            let c = x
3✔
486
                .to_kstr()
487
                .chars()
488
                .next()
489
                .map(|c| c.to_string())
2✔
490
                .unwrap_or_else(|| "".to_owned());
1✔
491
            Ok(Value::scalar(c))
1✔
492
        } else if let Some(x) = input.as_array() {
10✔
493
            Ok(x.first()
6✔
494
                .map(|v| v.to_value())
6✔
495
                .unwrap_or_else(|| Value::Nil))
2✔
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
)]
508
pub struct Last;
509

510
#[derive(Debug, Default, Display_filter)]
511
#[name = "last"]
512
struct LastFilter;
513

514
impl Filter for LastFilter {
515
    fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
2✔
516
        if let Some(x) = input.as_scalar() {
2✔
517
            let c = x
3✔
518
                .to_kstr()
519
                .chars()
520
                .last()
521
                .map(|c| c.to_string())
2✔
522
                .unwrap_or_else(|| "".to_owned());
1✔
523
            Ok(Value::scalar(c))
1✔
524
        } else if let Some(x) = input.as_array() {
8✔
525
            Ok(x.last().map(|v| v.to_value()).unwrap_or_else(|| Value::Nil))
13✔
526
        } else {
527
            Err(invalid_input("String or Array expected"))
×
528
        }
529
    }
530
}
531

532
#[cfg(test)]
533
mod 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
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc