1use 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
21pub 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
33pub 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
50pub 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#[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
233pub 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 #[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 #[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}