1use std::cmp;
2use std::fmt::Write;
3
4use liquid_core::model::try_find;
5use liquid_core::model::KStringCow;
6use liquid_core::model::ValueViewCmp;
7use liquid_core::parser::parse_variable;
8use liquid_core::Expression;
9use liquid_core::Result;
10use liquid_core::Runtime;
11use liquid_core::ValueCow;
12use liquid_core::{
13 Display_filter, Filter, FilterParameters, FilterReflection, FromFilterParameters, ParseFilter,
14};
15use liquid_core::{Value, ValueView};
16
17use crate::invalid_input;
18
19#[derive(Debug, Default, FilterParameters)]
20struct SortArgs {
21 #[parameter(description = "The property accessed by the filter.", arg_type = "str")]
22 property: Option<Expression>,
23 #[parameter(
24 description = "nils appear before or after non-nil values, either ('first' | 'last')",
25 arg_type = "str"
26 )]
27 nils: Option<Expression>,
28}
29
30#[derive(Clone, ParseFilter, FilterReflection)]
31#[filter(
32 name = "sort",
33 description = "Sorts items in an array. The order of the sorted array is case-sensitive.",
34 parameters(SortArgs),
35 parsed(SortFilter)
36)]
37pub struct Sort;
38
39#[derive(Debug, Default, FromFilterParameters, Display_filter)]
40#[name = "sort"]
41struct SortFilter {
42 #[parameters]
43 args: SortArgs,
44}
45
46#[derive(Copy, Clone)]
47enum NilsOrder {
48 First,
49 Last,
50}
51
52fn safe_property_getter<'v>(
53 value: &'v Value,
54 property: &KStringCow<'_>,
55 runtime: &dyn Runtime,
56) -> ValueCow<'v> {
57 let variable = parse_variable(property).expect("Failed to parse variable");
58 if let Some(path) = variable.try_evaluate(runtime) {
59 try_find(value, path.as_slice()).unwrap_or(ValueCow::Borrowed(&Value::Nil))
60 } else {
61 ValueCow::Borrowed(&Value::Nil)
62 }
63}
64
65fn nil_safe_compare(
66 a: &dyn ValueView,
67 b: &dyn ValueView,
68 nils: NilsOrder,
69) -> Option<cmp::Ordering> {
70 if a.is_nil() && b.is_nil() {
71 Some(cmp::Ordering::Equal)
72 } else if a.is_nil() {
73 match nils {
74 NilsOrder::First => Some(cmp::Ordering::Less),
75 NilsOrder::Last => Some(cmp::Ordering::Greater),
76 }
77 } else if b.is_nil() {
78 match nils {
79 NilsOrder::First => Some(cmp::Ordering::Greater),
80 NilsOrder::Last => Some(cmp::Ordering::Less),
81 }
82 } else {
83 ValueViewCmp::new(a).partial_cmp(&ValueViewCmp::new(b))
84 }
85}
86
87fn as_sequence<'k>(input: &'k dyn ValueView) -> Box<dyn Iterator<Item = &'k dyn ValueView> + 'k> {
88 if let Some(array) = input.as_array() {
89 array.values()
90 } else if input.is_nil() {
91 Box::new(vec![].into_iter())
92 } else {
93 Box::new(std::iter::once(input))
94 }
95}
96
97impl Filter for SortFilter {
98 fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
99 let args = self.args.evaluate(runtime)?;
100
101 let input: Vec<_> = as_sequence(input).collect();
102 if input.is_empty() {
103 return Err(invalid_input("Non-empty array expected"));
104 }
105 if args.property.is_some() && !input.iter().all(|v| v.is_object()) {
106 return Err(invalid_input("Array of objects expected"));
107 }
108 let nils = if let Some(nils) = &args.nils {
109 match nils.to_kstr().as_str() {
110 "first" => NilsOrder::First,
111 "last" => NilsOrder::Last,
112 _ => {
113 return Err(invalid_input(
114 "Invalid nils order. Must be \"first\" or \"last\".",
115 ))
116 }
117 }
118 } else {
119 NilsOrder::First
120 };
121
122 let mut sorted: Vec<Value> = input.iter().map(|v| v.to_value()).collect();
123 if let Some(property) = &args.property {
124 sorted.sort_by(|a, b| {
126 nil_safe_compare(
127 safe_property_getter(a, property, runtime).as_view(),
128 safe_property_getter(b, property, runtime).as_view(),
129 nils,
130 )
131 .unwrap_or(cmp::Ordering::Equal)
132 });
133 } else {
134 sorted.sort_by(|a, b| nil_safe_compare(a, b, nils).unwrap_or(cmp::Ordering::Equal));
135 }
136 Ok(Value::array(sorted))
137 }
138}
139
140#[derive(Debug, FilterParameters)]
141struct PushArgs {
142 #[parameter(description = "The element to append to the array.")]
143 element: Expression,
144}
145
146#[derive(Clone, ParseFilter, FilterReflection)]
147#[filter(
148 name = "push",
149 description = "Appends the given element to the end of an array.",
150 parameters(PushArgs),
151 parsed(PushFilter)
152)]
153pub struct Push;
154
155#[derive(Debug, FromFilterParameters, Display_filter)]
156#[name = "push"]
157struct PushFilter {
158 #[parameters]
159 args: PushArgs,
160}
161
162impl Filter for PushFilter {
163 fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
164 let args = self.args.evaluate(runtime)?;
165
166 let element = args.element.to_value();
167 let mut array = input
168 .to_value()
169 .into_array()
170 .ok_or_else(|| invalid_input("Array expected"))?;
171 array.push(element);
172
173 Ok(Value::Array(array))
174 }
175}
176
177#[derive(Clone, ParseFilter, FilterReflection)]
178#[filter(
179 name = "pop",
180 description = "Removes the last element of an array.",
181 parsed(PopFilter)
182)]
183pub struct Pop;
184
185#[derive(Debug, Default, Display_filter)]
186#[name = "pop"]
187struct PopFilter;
188
189impl Filter for PopFilter {
190 fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
191 let mut array = input
192 .to_value()
193 .into_array()
194 .ok_or_else(|| invalid_input("Array expected"))?;
195 array.pop();
196
197 Ok(Value::Array(array))
198 }
199}
200
201#[derive(Debug, FilterParameters)]
202struct UnshiftArgs {
203 #[parameter(description = "The element to append to the array.")]
204 element: Expression,
205}
206
207#[derive(Clone, ParseFilter, FilterReflection)]
208#[filter(
209 name = "unshift",
210 description = "Appends the given element to the start of an array.",
211 parameters(UnshiftArgs),
212 parsed(UnshiftFilter)
213)]
214pub struct Unshift;
215
216#[derive(Debug, FromFilterParameters, Display_filter)]
217#[name = "unshift"]
218struct UnshiftFilter {
219 #[parameters]
220 args: UnshiftArgs,
221}
222
223impl Filter for UnshiftFilter {
224 fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
225 let args = self.args.evaluate(runtime)?;
226
227 let element = args.element.to_value();
228 let mut array = input
229 .to_value()
230 .into_array()
231 .ok_or_else(|| invalid_input("Array expected"))?;
232 array.insert(0, element);
233
234 Ok(Value::Array(array))
235 }
236}
237
238#[derive(Clone, ParseFilter, FilterReflection)]
239#[filter(
240 name = "shift",
241 description = "Removes the first element of an array.",
242 parsed(ShiftFilter)
243)]
244pub struct Shift;
245
246#[derive(Debug, Default, Display_filter)]
247#[name = "shift"]
248struct ShiftFilter;
249
250impl Filter for ShiftFilter {
251 fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result<Value> {
252 let mut array = input
253 .to_value()
254 .into_array()
255 .ok_or_else(|| invalid_input("Array expected"))?;
256
257 if !array.is_empty() {
258 array.remove(0);
259 }
260
261 Ok(Value::Array(array))
262 }
263}
264
265#[derive(Debug, FilterParameters)]
266struct ArrayToSentenceStringArgs {
267 #[parameter(
268 description = "The connector between the last two elements. Defaults to \"and\".",
269 arg_type = "str"
270 )]
271 connector: Option<Expression>,
272}
273
274#[derive(Clone, ParseFilter, FilterReflection)]
275#[filter(
276 name = "array_to_sentence_string",
277 description = "Converts an array into a sentence. This sentence will be a list of the elements of the array separated by comma, with a connector between the last two elements.",
278 parameters(ArrayToSentenceStringArgs),
279 parsed(ArrayToSentenceStringFilter)
280)]
281pub struct ArrayToSentenceString;
282
283#[derive(Debug, FromFilterParameters, Display_filter)]
284#[name = "array_to_sentence_string"]
285struct ArrayToSentenceStringFilter {
286 #[parameters]
287 args: ArrayToSentenceStringArgs,
288}
289
290impl Filter for ArrayToSentenceStringFilter {
291 fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result<Value> {
292 let args = self.args.evaluate(runtime)?;
293
294 let connector = args.connector.unwrap_or_else(|| "and".into());
295
296 let mut array = input
297 .as_array()
298 .ok_or_else(|| invalid_input("Array expected"))?
299 .values();
300
301 let mut sentence = array
302 .next()
303 .map(|v| v.to_kstr().into_string())
304 .unwrap_or_else(|| "".to_owned());
305
306 let mut iter = array.peekable();
307 while let Some(value) = iter.next() {
308 if iter.peek().is_some() {
309 write!(sentence, ", {}", value.render())
310 .expect("It should be safe to write to a string.");
311 } else {
312 write!(sentence, ", {} {}", connector, value.render())
313 .expect("It should be safe to write to a string.");
314 }
315 }
316
317 Ok(Value::scalar(sentence))
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn unit_sort() {
327 let input = &liquid_core::value!(["Z", "b", "c", "a"]);
328 let desired_result = liquid_core::value!(["Z", "a", "b", "c"]);
329 assert_eq!(
330 liquid_core::call_filter!(Sort, input).unwrap(),
331 desired_result
332 );
333 }
334
335 #[test]
336 fn unit_push() {
337 let input = liquid_core::value!(["Seattle", "Tacoma"]);
338 let unit_result = liquid_core::call_filter!(Push, input, "Spokane").unwrap();
339 let desired_result = liquid_core::value!(["Seattle", "Tacoma", "Spokane",]);
340 assert_eq!(unit_result, desired_result);
341 }
342
343 #[test]
344 fn unit_pop() {
345 let input = liquid_core::value!(["Seattle", "Tacoma"]);
346 let unit_result = liquid_core::call_filter!(Pop, input).unwrap();
347 let desired_result = liquid_core::value!(["Seattle"]);
348 assert_eq!(unit_result, desired_result);
349 }
350
351 #[test]
352 fn unit_pop_empty() {
353 let input = liquid_core::value!([]);
354 let unit_result = liquid_core::call_filter!(Pop, input).unwrap();
355 let desired_result = liquid_core::value!([]);
356 assert_eq!(unit_result, desired_result);
357 }
358
359 #[test]
360 fn unit_unshift() {
361 let input = liquid_core::value!(["Seattle", "Tacoma"]);
362 let unit_result = liquid_core::call_filter!(Unshift, input, "Olympia").unwrap();
363 let desired_result = liquid_core::value!(["Olympia", "Seattle", "Tacoma"]);
364 assert_eq!(unit_result, desired_result);
365 }
366
367 #[test]
368 fn unit_shift() {
369 let input = liquid_core::value!(["Seattle", "Tacoma"]);
370 let unit_result = liquid_core::call_filter!(Shift, input).unwrap();
371 let desired_result = liquid_core::value!(["Tacoma"]);
372 assert_eq!(unit_result, desired_result);
373 }
374
375 #[test]
376 fn unit_shift_empty() {
377 let input = liquid_core::value!([]);
378 let unit_result = liquid_core::call_filter!(Shift, input).unwrap();
379 let desired_result = liquid_core::value!([]);
380 assert_eq!(unit_result, desired_result);
381 }
382
383 #[test]
384 fn unit_array_to_sentence_string() {
385 let input = liquid_core::value!(["foo", "bar", "baz"]);
386 let unit_result = liquid_core::call_filter!(ArrayToSentenceString, input).unwrap();
387 let desired_result = "foo, bar, and baz";
388 assert_eq!(unit_result, desired_result);
389 }
390
391 #[test]
392 fn unit_array_to_sentence_string_two_elements() {
393 let input = liquid_core::value!(["foo", "bar"]);
394 let unit_result = liquid_core::call_filter!(ArrayToSentenceString, input).unwrap();
395 let desired_result = "foo, and bar";
396 assert_eq!(unit_result, desired_result);
397 }
398
399 #[test]
400 fn unit_array_to_sentence_string_one_element() {
401 let input = liquid_core::value!(["foo"]);
402 let unit_result = liquid_core::call_filter!(ArrayToSentenceString, input).unwrap();
403 let desired_result = "foo";
404 assert_eq!(unit_result, desired_result);
405 }
406
407 #[test]
408 fn unit_array_to_sentence_string_no_elements() {
409 let input = liquid_core::value!([]);
410 let unit_result = liquid_core::call_filter!(ArrayToSentenceString, input).unwrap();
411 let desired_result = "";
412 assert_eq!(unit_result, desired_result);
413 }
414
415 #[test]
416 fn unit_array_to_sentence_string_custom_connector() {
417 let input = liquid_core::value!(["foo", "bar", "baz"]);
418 let unit_result = liquid_core::call_filter!(ArrayToSentenceString, input, "or").unwrap();
419 let desired_result = "foo, bar, or baz";
420 assert_eq!(unit_result, desired_result);
421 }
422}