tera/builtins/filters/
common.rs

1/// Filters operating on multiple types
2use std::collections::HashMap;
3#[cfg(feature = "date-locale")]
4use std::convert::TryFrom;
5use std::iter::FromIterator;
6
7use crate::errors::{Error, Result};
8use crate::utils::render_to_string;
9#[cfg(feature = "builtins")]
10use chrono::{
11    format::{Item, StrftimeItems},
12    DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone, Utc,
13};
14#[cfg(feature = "builtins")]
15use chrono_tz::Tz;
16use serde_json::value::{to_value, Value};
17use serde_json::{to_string, to_string_pretty};
18
19use crate::context::ValueRender;
20
21// Returns the number of items in an array or an object, or the number of characters in a string.
22pub fn length(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
23    match value {
24        Value::Array(arr) => Ok(to_value(arr.len()).unwrap()),
25        Value::Object(m) => Ok(to_value(m.len()).unwrap()),
26        Value::String(s) => Ok(to_value(s.chars().count()).unwrap()),
27        _ => Err(Error::msg(
28            "Filter `length` was used on a value that isn't an array, an object, or a string.",
29        )),
30    }
31}
32
33// Reverses the elements of an array or the characters in a string.
34pub fn reverse(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
35    match value {
36        Value::Array(arr) => {
37            let mut rev = arr.clone();
38            rev.reverse();
39            to_value(&rev).map_err(Error::json)
40        }
41        Value::String(s) => to_value(String::from_iter(s.chars().rev())).map_err(Error::json),
42        _ => Err(Error::msg(format!(
43            "Filter `reverse` received an incorrect type for arg `value`: \
44             got `{}` but expected Array|String",
45            value
46        ))),
47    }
48}
49
50// Encodes a value of any type into json, optionally `pretty`-printing it
51// `pretty` can be true to enable pretty-print, or omitted for compact printing
52pub fn json_encode(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
53    let pretty = args.get("pretty").and_then(Value::as_bool).unwrap_or(false);
54
55    if pretty {
56        to_string_pretty(&value).map(Value::String).map_err(Error::json)
57    } else {
58        to_string(&value).map(Value::String).map_err(Error::json)
59    }
60}
61
62/// Returns a formatted time according to the given `format` argument.
63/// `format` defaults to the ISO 8601 `YYYY-MM-DD` format.
64///
65/// Input can be an i64 timestamp (seconds since epoch) or an RFC3339 string
66/// (default serialization format for `chrono::DateTime`).
67///
68/// a full reference for the time formatting syntax is available
69/// on [chrono docs](https://lifthrasiir.github.io/rust-chrono/chrono/format/strftime/index.html)
70#[cfg(feature = "builtins")]
71pub fn date(value: &Value, args: &HashMap<String, Value>) -> Result<Value> {
72    let format = match args.get("format") {
73        Some(val) => try_get_value!("date", "format", String, val),
74        None => "%Y-%m-%d".to_string(),
75    };
76
77    let items: Vec<Item> =
78        StrftimeItems::new(&format).filter(|item| matches!(item, Item::Error)).collect();
79    if !items.is_empty() {
80        return Err(Error::msg(format!("Invalid date format `{}`", format)));
81    }
82
83    let timezone = match args.get("timezone") {
84        Some(val) => {
85            let timezone = try_get_value!("date", "timezone", String, val);
86            match timezone.parse::<Tz>() {
87                Ok(timezone) => Some(timezone),
88                Err(_) => {
89                    return Err(Error::msg(format!("Error parsing `{}` as a timezone", timezone)))
90                }
91            }
92        }
93        None => None,
94    };
95
96    #[cfg(feature = "date-locale")]
97    let formatted = {
98        let locale = match args.get("locale") {
99            Some(val) => {
100                let locale = try_get_value!("date", "locale", String, val);
101                chrono::Locale::try_from(locale.as_str())
102                    .map_err(|_| Error::msg(format!("Error parsing `{}` as a locale", locale)))?
103            }
104            None => chrono::Locale::POSIX,
105        };
106        match value {
107            Value::Number(n) => match n.as_i64() {
108                Some(i) => {
109                    let date = NaiveDateTime::from_timestamp_opt(i, 0).expect(
110                        "out of bound seconds should not appear, as we set nanoseconds to zero",
111                    );
112                    match timezone {
113                        Some(timezone) => {
114                            timezone.from_utc_datetime(&date).format_localized(&format, locale)
115                        }
116                        None => date.format(&format),
117                    }
118                }
119                None => {
120                    return Err(Error::msg(format!("Filter `date` was invoked on a float: {}", n)))
121                }
122            },
123            Value::String(s) => {
124                if s.contains('T') {
125                    match s.parse::<DateTime<FixedOffset>>() {
126                        Ok(val) => match timezone {
127                            Some(timezone) => {
128                                val.with_timezone(&timezone).format_localized(&format, locale)
129                            }
130                            None => val.format_localized(&format, locale),
131                        },
132                        Err(_) => match s.parse::<NaiveDateTime>() {
133                            Ok(val) => DateTime::<Utc>::from_naive_utc_and_offset(val, Utc)
134                                .format_localized(&format, locale),
135                            Err(_) => {
136                                return Err(Error::msg(format!(
137                                    "Error parsing `{:?}` as rfc3339 date or naive datetime",
138                                    s
139                                )));
140                            }
141                        },
142                    }
143                } else {
144                    match NaiveDate::parse_from_str(s, "%Y-%m-%d") {
145                        Ok(val) => DateTime::<Utc>::from_naive_utc_and_offset(
146                            val.and_hms_opt(0, 0, 0).expect(
147                                "out of bound should not appear, as we set the time to zero",
148                            ),
149                            Utc,
150                        )
151                        .format_localized(&format, locale),
152                        Err(_) => {
153                            return Err(Error::msg(format!(
154                                "Error parsing `{:?}` as YYYY-MM-DD date",
155                                s
156                            )));
157                        }
158                    }
159                }
160            }
161            _ => {
162                return Err(Error::msg(format!(
163                    "Filter `date` received an incorrect type for arg `value`: \
164                     got `{:?}` but expected i64|u64|String",
165                    value
166                )));
167            }
168        }
169    };
170
171    #[cfg(not(feature = "date-locale"))]
172    let formatted = match value {
173        Value::Number(n) => match n.as_i64() {
174            Some(i) => {
175                let date = NaiveDateTime::from_timestamp_opt(i, 0).expect(
176                    "out of bound seconds should not appear, as we set nanoseconds to zero",
177                );
178                match timezone {
179                    Some(timezone) => timezone.from_utc_datetime(&date).format(&format),
180                    None => date.format(&format),
181                }
182            }
183            None => return Err(Error::msg(format!("Filter `date` was invoked on a float: {}", n))),
184        },
185        Value::String(s) => {
186            if s.contains('T') {
187                match s.parse::<DateTime<FixedOffset>>() {
188                    Ok(val) => match timezone {
189                        Some(timezone) => val.with_timezone(&timezone).format(&format),
190                        None => val.format(&format),
191                    },
192                    Err(_) => match s.parse::<NaiveDateTime>() {
193                        Ok(val) => {
194                            DateTime::<Utc>::from_naive_utc_and_offset(val, Utc).format(&format)
195                        }
196                        Err(_) => {
197                            return Err(Error::msg(format!(
198                                "Error parsing `{:?}` as rfc3339 date or naive datetime",
199                                s
200                            )));
201                        }
202                    },
203                }
204            } else {
205                match NaiveDate::parse_from_str(s, "%Y-%m-%d") {
206                    Ok(val) => DateTime::<Utc>::from_naive_utc_and_offset(
207                        val.and_hms_opt(0, 0, 0)
208                            .expect("out of bound should not appear, as we set the time to zero"),
209                        Utc,
210                    )
211                    .format(&format),
212                    Err(_) => {
213                        return Err(Error::msg(format!(
214                            "Error parsing `{:?}` as YYYY-MM-DD date",
215                            s
216                        )));
217                    }
218                }
219            }
220        }
221        _ => {
222            return Err(Error::msg(format!(
223                "Filter `date` received an incorrect type for arg `value`: \
224                 got `{:?}` but expected i64|u64|String",
225                value
226            )));
227        }
228    };
229
230    to_value(formatted.to_string()).map_err(Error::json)
231}
232
233// Returns the given value as a string.
234pub fn as_str(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
235    let value =
236        render_to_string(|| format!("as_str for value of kind {}", value), |w| value.render(w))?;
237    to_value(value).map_err(Error::json)
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    #[cfg(feature = "builtins")]
244    use chrono::{DateTime, Local};
245    use serde_json;
246    use serde_json::value::to_value;
247    use std::collections::HashMap;
248
249    #[test]
250    fn as_str_object() {
251        let map: HashMap<String, String> = HashMap::new();
252        let result = as_str(&to_value(map).unwrap(), &HashMap::new());
253        assert!(result.is_ok());
254        assert_eq!(result.unwrap(), to_value("[object]").unwrap());
255    }
256
257    #[test]
258    fn as_str_vec() {
259        let result = as_str(&to_value(vec![1, 2, 3, 4]).unwrap(), &HashMap::new());
260        assert!(result.is_ok());
261        assert_eq!(result.unwrap(), to_value("[1, 2, 3, 4]").unwrap());
262    }
263
264    #[test]
265    fn length_vec() {
266        let result = length(&to_value(vec![1, 2, 3, 4]).unwrap(), &HashMap::new());
267        assert!(result.is_ok());
268        assert_eq!(result.unwrap(), to_value(4).unwrap());
269    }
270
271    #[test]
272    fn length_object() {
273        let mut map: HashMap<String, String> = HashMap::new();
274        map.insert("foo".to_string(), "bar".to_string());
275        let result = length(&to_value(&map).unwrap(), &HashMap::new());
276        assert!(result.is_ok());
277        assert_eq!(result.unwrap(), to_value(1).unwrap());
278    }
279
280    #[test]
281    fn length_str() {
282        let result = length(&to_value("Hello World").unwrap(), &HashMap::new());
283        assert!(result.is_ok());
284        assert_eq!(result.unwrap(), to_value(11).unwrap());
285    }
286
287    #[test]
288    fn length_str_nonascii() {
289        let result = length(&to_value("日本語").unwrap(), &HashMap::new());
290        assert!(result.is_ok());
291        assert_eq!(result.unwrap(), to_value(3).unwrap());
292    }
293
294    #[test]
295    fn length_num() {
296        let result = length(&to_value(15).unwrap(), &HashMap::new());
297        assert!(result.is_err());
298    }
299
300    #[test]
301    fn reverse_vec() {
302        let result = reverse(&to_value(vec![1, 2, 3, 4]).unwrap(), &HashMap::new());
303        assert!(result.is_ok());
304        assert_eq!(result.unwrap(), to_value(vec![4, 3, 2, 1]).unwrap());
305    }
306
307    #[test]
308    fn reverse_str() {
309        let result = reverse(&to_value("Hello World").unwrap(), &HashMap::new());
310        assert!(result.is_ok());
311        assert_eq!(result.unwrap(), to_value("dlroW olleH").unwrap());
312    }
313
314    #[test]
315    fn reverse_num() {
316        let result = reverse(&to_value(1.23).unwrap(), &HashMap::new());
317        assert!(result.is_err());
318        assert_eq!(
319            result.err().unwrap().to_string(),
320            "Filter `reverse` received an incorrect type for arg `value`: got `1.23` but expected Array|String"
321        );
322    }
323
324    #[cfg(feature = "builtins")]
325    #[test]
326    fn date_default() {
327        let args = HashMap::new();
328        let result = date(&to_value(1482720453).unwrap(), &args);
329        assert!(result.is_ok());
330        assert_eq!(result.unwrap(), to_value("2016-12-26").unwrap());
331    }
332
333    #[cfg(feature = "builtins")]
334    #[test]
335    fn date_custom_format() {
336        let mut args = HashMap::new();
337        args.insert("format".to_string(), to_value("%Y-%m-%d %H:%M").unwrap());
338        let result = date(&to_value(1482720453).unwrap(), &args);
339        assert!(result.is_ok());
340        assert_eq!(result.unwrap(), to_value("2016-12-26 02:47").unwrap());
341    }
342
343    // https://zola.discourse.group/t/can-i-generate-a-random-number-within-a-range/238?u=keats
344    // https://github.com/chronotope/chrono/issues/47
345    #[cfg(feature = "builtins")]
346    #[test]
347    fn date_errors_on_incorrect_format() {
348        let mut args = HashMap::new();
349        args.insert("format".to_string(), to_value("%2f").unwrap());
350        let result = date(&to_value(1482720453).unwrap(), &args);
351        assert!(result.is_err());
352    }
353
354    #[cfg(feature = "builtins")]
355    #[test]
356    fn date_rfc3339() {
357        let args = HashMap::new();
358        let dt: DateTime<Local> = Local::now();
359        let result = date(&to_value(dt.to_rfc3339()).unwrap(), &args);
360        assert!(result.is_ok());
361        assert_eq!(result.unwrap(), to_value(dt.format("%Y-%m-%d").to_string()).unwrap());
362    }
363
364    #[cfg(feature = "builtins")]
365    #[test]
366    fn date_rfc3339_preserves_timezone() {
367        let mut args = HashMap::new();
368        args.insert("format".to_string(), to_value("%Y-%m-%d %z").unwrap());
369        let result = date(&to_value("1996-12-19T16:39:57-08:00").unwrap(), &args);
370        assert!(result.is_ok());
371        assert_eq!(result.unwrap(), to_value("1996-12-19 -0800").unwrap());
372    }
373
374    #[cfg(feature = "builtins")]
375    #[test]
376    fn date_yyyy_mm_dd() {
377        let mut args = HashMap::new();
378        args.insert("format".to_string(), to_value("%a, %d %b %Y %H:%M:%S %z").unwrap());
379        let result = date(&to_value("2017-03-05").unwrap(), &args);
380        assert!(result.is_ok());
381        assert_eq!(result.unwrap(), to_value("Sun, 05 Mar 2017 00:00:00 +0000").unwrap());
382    }
383
384    #[cfg(feature = "builtins")]
385    #[test]
386    fn date_from_naive_datetime() {
387        let mut args = HashMap::new();
388        args.insert("format".to_string(), to_value("%a, %d %b %Y %H:%M:%S").unwrap());
389        let result = date(&to_value("2017-03-05T00:00:00.602").unwrap(), &args);
390        println!("{:?}", result);
391        assert!(result.is_ok());
392        assert_eq!(result.unwrap(), to_value("Sun, 05 Mar 2017 00:00:00").unwrap());
393    }
394
395    // https://github.com/getzola/zola/issues/1279
396    #[cfg(feature = "builtins")]
397    #[test]
398    fn date_format_doesnt_panic() {
399        let mut args = HashMap::new();
400        args.insert("format".to_string(), to_value("%+S").unwrap());
401        let result = date(&to_value("2017-01-01T00:00:00").unwrap(), &args);
402        assert!(result.is_ok());
403    }
404
405    #[cfg(feature = "builtins")]
406    #[test]
407    fn date_with_timezone() {
408        let mut args = HashMap::new();
409        args.insert("timezone".to_string(), to_value("America/New_York").unwrap());
410        let result = date(&to_value("2019-09-19T01:48:44.581Z").unwrap(), &args);
411        assert!(result.is_ok());
412        assert_eq!(result.unwrap(), to_value("2019-09-18").unwrap());
413    }
414
415    #[cfg(feature = "builtins")]
416    #[test]
417    fn date_with_invalid_timezone() {
418        let mut args = HashMap::new();
419        args.insert("timezone".to_string(), to_value("Narnia").unwrap());
420        let result = date(&to_value("2019-09-19T01:48:44.581Z").unwrap(), &args);
421        assert!(result.is_err());
422        assert_eq!(result.err().unwrap().to_string(), "Error parsing `Narnia` as a timezone");
423    }
424
425    #[cfg(feature = "builtins")]
426    #[test]
427    fn date_timestamp() {
428        let mut args = HashMap::new();
429        args.insert("format".to_string(), to_value("%Y-%m-%d").unwrap());
430        let result = date(&to_value(1648302603).unwrap(), &args);
431        assert!(result.is_ok());
432        assert_eq!(result.unwrap(), to_value("2022-03-26").unwrap());
433    }
434
435    #[cfg(feature = "builtins")]
436    #[test]
437    fn date_timestamp_with_timezone() {
438        let mut args = HashMap::new();
439        args.insert("timezone".to_string(), to_value("Europe/Berlin").unwrap());
440        let result = date(&to_value(1648252203).unwrap(), &args);
441        assert!(result.is_ok());
442        assert_eq!(result.unwrap(), to_value("2022-03-26").unwrap());
443    }
444
445    #[cfg(feature = "date-locale")]
446    #[test]
447    fn date_timestamp_with_timezone_and_locale() {
448        let mut args = HashMap::new();
449        args.insert("format".to_string(), to_value("%A %-d %B").unwrap());
450        args.insert("timezone".to_string(), to_value("Europe/Paris").unwrap());
451        args.insert("locale".to_string(), to_value("fr_FR").unwrap());
452        let result = date(&to_value(1659817310).unwrap(), &args);
453        assert!(result.is_ok());
454        assert_eq!(result.unwrap(), to_value("samedi 6 août").unwrap());
455    }
456
457    #[cfg(feature = "date-locale")]
458    #[test]
459    fn date_with_invalid_locale() {
460        let mut args = HashMap::new();
461        args.insert("locale".to_string(), to_value("xx_XX").unwrap());
462        let result = date(&to_value("2019-09-19T01:48:44.581Z").unwrap(), &args);
463        assert!(result.is_err());
464        assert_eq!(result.err().unwrap().to_string(), "Error parsing `xx_XX` as a locale");
465    }
466
467    #[test]
468    fn test_json_encode() {
469        let args = HashMap::new();
470        let result =
471            json_encode(&serde_json::from_str("{\"key\": [\"value1\", 2, true]}").unwrap(), &args);
472        assert!(result.is_ok());
473        assert_eq!(result.unwrap(), to_value("{\"key\":[\"value1\",2,true]}").unwrap());
474    }
475
476    #[test]
477    fn test_json_encode_pretty() {
478        let mut args = HashMap::new();
479        args.insert("pretty".to_string(), to_value(true).unwrap());
480        let result =
481            json_encode(&serde_json::from_str("{\"key\": [\"value1\", 2, true]}").unwrap(), &args);
482        assert!(result.is_ok());
483        assert_eq!(
484            result.unwrap(),
485            to_value("{\n  \"key\": [\n    \"value1\",\n    2,\n    true\n  ]\n}").unwrap()
486        );
487    }
488}